java泛型详解

本文深入讲解Java泛型的原理和应用,包括泛型的优势、如何使用泛型、泛型方法、泛型擦除、泛型边界及通配符等核心概念。通过实例演示如何将普通类转换为泛型类,探讨泛型擦除的影响,以及如何利用泛型边界和通配符增强代码的灵活性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一,java泛型简介
java泛型避免了运行期错误,在编译时对代码进行了检查。在新代码中我们不应该使用原生态类型,否则就失掉了泛型在安全性和表述性方面的所有优势。
泛型不像数组,可以支持协变。比如Apple是Fruit的子类型。但是List不是List的子类型。这个问题怎么解决,我们将在五六小节详细说明。
再一方面,java里是没有真正的泛型的,所以java的泛型也叫做伪泛型。泛型在运行期都会擦除。这个知识点我们将在泛型擦除这一小节详细介绍。
二,学会使用泛型
举一个effective版中的一个简单例子。
首先,这是一个简单的堆栈实现,代码如下

package stackDemo;

import java.util.Arrays;
import java.util.EmptyStackException;

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULLT_INITIAL_CAPACITY = 16;

    public Stack(Object[] elements) {
        elements = new Object[DEFAULLT_INITIAL_CAPACITY];
    }

    public void push(Object e){
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if(size == 0){
            throw new EmptyStackException();
        }
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }

    public void ensureCapacity() {
        if(elements.length == size){
            elements = Arrays.copyOf(elements,2*size+1);
        }
    }
}

将类泛型化的第一个步骤是给它的声明添加一个或者多个类型参数。下一步是用相应的类型参数替换所有的Object类型。
修改后,代码如下

package stackDemo;

import java.util.Arrays;
import java.util.EmptyStackException;

public class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULLT_INITIAL_CAPACITY = 16;

    public Stack(E[] elements) {
        elements = new E[DEFAULLT_INITIAL_CAPACITY];
    }

    public void push(E e){
        ensureCapacity();
        elements[size++] = e;
    }

    public E pop() {
        if(size == 0){
            throw new EmptyStackException();
        }
        E result = elements[--size];
        elements[size] = null;
        return result;
    }

    public void ensureCapacity() {
        if(elements.length == size){
            elements = Arrays.copyOf(elements,2*size+1);
        }
    }
}

这里呢,编译器会产生一个错误

        elements = new E[DEFAULLT_INITIAL_CAPACITY];
stackDemo/Stack.java:13

因为我们不能创建不可具体化类型的数组,数组是不支持泛型的。effective java一书中给出了两种解决方案,这里只写第二种。
将elements域的类型从E[]改为Object[]。
然后将pop方法改动如下

 @SuppressWarnings("unchecked")
        E result = (E)elements[--size];

三,泛型方法
首先,泛型方法能够独立于类而产生变化,是否是泛型方法与其所在的类是否是泛型没关系。
其次,无论何时,只要能够做到,就应该尽量使用泛型方法。另外,对于static方法而言,无法访问泛型类的类型参数。所以,如果static方法需要使用泛型能力,就必须使其成为泛型方法。
简单举两个泛型方法的例子来加深大家的理解

public static <T>  void union(Set<T> s1,Set<T> s2){
    Set<T> result = new HashSet(s1);
    result.addAll(s2);
    return result;
}
public static <T extends Comparable> T max(List<T> list){
   Iterator<T> i = list.iterator();
   T result = i.next();
   while(i.hasNext()){
     T t = i.next();
     if(t.compareTo(result) > 0){
       result = t;
     }
   }
   return result;
}

四,泛型擦除
泛型技术在C#和java中有着根本性的分歧。C#里面的泛型无论是在源码中或是运行期的CLR中,都是切实存在的。List与List就是不同的类型。它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型。
java语言中的泛型则不同,它只在程序源码中存在,在编译后的字节码文件中就已经替换为原来的原生类型。所以java语言的泛型技术本质上来说就是一颗语法糖。
泛型擦除,一般来说类型参数将会被替换为Object类型,但如果是下面这种情况

Class A<T extends B>

那么泛型类型参数将擦除到它的第一个边界。(可能会有多个边界)
再一点,泛型擦除的代价是显著的,因为它不能显示地引用运行时类型的操作中,例如转型,instance of和new 表达式。因为所有关于参数的类型信息都丢失了。所以我们写代码时必须时刻提醒自己,我们只是看起来好像拥有有关参数的类型信息而已。
擦除既然有缺陷,java也相应的对它进行了补偿
例如,我们不能用instanceof来判断类型,因为其类型信息已被擦除。我们可以转而使用动态的isInstance()。
例子如下

class Building{}
class House extends Building{}
public classs ClassTypeCapture<T>{
   Class<T>  kind;
   public ClassTypeCapture(Class<T> kind){
      this.kind = kind;
   }
   public boolean f(Object arg){
     return kind.isInstance(arg);
   }
   public static void main(String[] args){
      ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<Building>(Building.class);
      system.out.println(cttq1.f(new Building()));
      system.out.println(cttq1.f(new House()));
   } 
   true
   true
   
}

java中对于new T()这种尝试将无法实现,一部分原因是因为擦除,另一部分原因是因为编译器不能验证T具有默认(无参)构造器。但是在C#中,这种操作很自然,并且很安全。
java中的解决方案是传递一个工厂对象,并使用它来创建新的实例。最方便的工厂对象就是Class对象,因此如果使用类型标签,那么你就可以使用newInstance()来创建这个类型的新对象。

class ClassAsFactory<T>{
   T x;
   public ClassAsFactory(Class<T>  kind){
     try{
      x = kind.newInstance();
     }catch(Exception e){
       throw new RuntimeException(e);
    }
   }
}
class Employee{}
public class InstantiateGenericType{
   public static void main(String[] args){
     ClassAsFactory<Employee> fe = new ClassAsFactory<Employee>(Employee.class);
   }
}

再一个方式是使用显示的工厂,并限制其类型,这种方式就是类似Spring中的FactoryBean。实现如下

Interface Factory<T>{
  T create();
}
class Foo2<T>{
   private T x;
   public <F extends Factory<T>> foo2(F factory){
     x = factory.create();
   }
   //........
}
class IntegerFactory implements Factory<Integer>{
   public Integer create(){
      return new Integer(0);
   }
}
class Widget{
  public static class Factory implements Factory<Widget>{
      pulic Widget create(){
         return new Widget();
      }
  }
}
public class FactoryConstraint{
   public static void main(String[] args){
      new Foo2<Integer>(new IntegerFactory());
      new Foo2<Widget>(new Widget.Factory());
   }
}

五,泛型边界
边界使得你可以在泛型的类型参数上设置限制条件,因为泛型擦除,所以无界泛型参数只可以调用Object的方法。但是,如果能够将这个参数限制为某个类型子集,那么你就可以用这些类型子集来调用方法。为了执行这种限制,Java泛型重用了extends关键字。

package com.zy.test;
 
import java.awt.Color;
 
public class BasicBounds {
    public static void main(String[] args) {
        Solid<Bounded> solid = new Solid<Bounded>(new Bounded());
        System.out.println(solid.color());
        System.out.println(solid.getY());
        System.out.println(solid.weight());
    }
}
 
interface HasColor {
    java.awt.Color getColor();
}
 
class Colored<T extends HasColor> {
    T item;
    Colored(T item) {
        this.item = item;
    }
    T getItem() {
        return item;
    }
    java.awt.Color color() {
        return item.getColor();
    }
}
 
class Dimension {
    public int x, y, z;
}
 
class ColoredDimension<T extends Dimension & HasColor> {// 类和接口都是放进来
    T item;
    public T getItem() {
        return item;
    }
    public void setItem(T item) {
        this.item = item;
    }
    ColoredDimension(T item) {
        this.item = item;
    }
    
    java.awt.Color color() {
        return item.getColor();
    }
    
    int getX() {
        return item.x;
    }
    
    int getY() {
        return item.y;
    }
    
    
    int getZ() {
        return item.z;
    }
}
 
interface Weight {
    int weight();
}
 
class Solid<T extends Dimension & HasColor & Weight> {
    T item;
 
    Solid(T item) {
        this.item = item;
    }
    
    public T getItem() {
        return item;
    }
    
    java.awt.Color color() {
        return item.getColor();
    }
    
    int getX() {
        return item.x;
    }
    
    int getY() {
        return item.y;
    }
    
    
    int getZ() {
        return item.z;
    }
    
    int weight() {
        return item.weight();
    }
}
 
 
class Bounded extends Dimension implements HasColor, Weight {
    
    @Override
    public int weight() {
        return 0;
    }
 
    @Override
    public Color getColor() {
        return null;
    }
    
}

六,泛型通配符
通配符分为三种
上界通配符,下界通配符,无界通配符
先讲有界通配符,简单来说,有这么两点
1.上界<? extends T>不能往里存,只能往外取
2.下界<? super T>往外取只能赋值给Object变量,不影响往里存

我们在本文一开始就说过,泛型和数组不一样,是不支持协变的。而通配符则能够在两个类型之间建立某种向上的转型关系。它能够给我们以最大限度的灵活性。
那我们怎样去使用有界通配符呢?在effective java中给出了一种简单高效的方法 名为"PECS",即 producer- extends ,consumer -super。
换句话说,如果参数化类型表示一个T生产者,就使用<? extends T>;如果它表示一个T消费者,就使用<? super T>。
举个例子

public static <E> Set<E> union (Set<E> s1,Set<E> s2){
  Set<E> result = new HashSet(s1);
  result.addAll(s2);
  return result;
}

s1和s2着两个参数都是生产者,根据PECS规则,这个声明应该是

public static <E> Set<E> union (Set<? extends E> s1,Set<? extends E> s2)

书中提到,Comparable始终是消费者,因此使用时始终应该是Comparable<? super T>优先于Comparable。对于Comparator也一样。
举例如下

    public static <T extends Comparable<? super T>> T max(List<? extends T> list){
        Iterator<? extends T> t = list.iterator();
        T result = t.next();
        while (t.hasNext()){
            T i = t.next();
            if(i.compareTo(result) > 0){
                result = i;
            }
        }
        return result;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值