Java泛型浅谈

Java泛型浅谈

为什么需要泛型

关于Java中引入泛型的原因,我认为Java核心技术第十版中的8.1阐述的非常通俗易懂。下面我以书中ArrayList的例子简单说明一下(ps.可以在微信读书上去看这本书哦,免费!!!)

我们知道,ArrayList是一个可以聚集任何类型的数组列表。但是不知道大家有没有注意过ArrayList是如何实现任意类型聚集的,查看ArrayList源码,答案显而易见就是通过泛型进行聚集的。

ArrayList类的声明

实际上在Java SE 5.0增加泛型机制之前,就已经存在一个ArrayList类,彼时的ArrayList是通过继承关系实现任意类型聚集的。

Java SE 5.0之前的ArrayList

通过这样的方式获取元素时,必须进行强制类型转换(String str = (String)arraylist.get(i))。添加元素时,没有错误检查,可以向列表中添加任意类型的元素(arraylist.add(new Integer(1)))。对于这样的调用,单看这个语句,编译与运行都不会有问题。但当在获取这个数组列表元素并进行强制类型转换时,就会得到一个类型转换异常。

而使用泛型机制实现ArrayList就不会出现上述的问题。

//JDK1.8中的ArrayList实现
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{	
	transient Object[] elementData; 
	...
	public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }
	public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    ...
}

在获取元素时,我们不需要进行强制类型转换,编译器会根据定义ArrayList的变量时给出的数据类型自动判断返回值类型。添加元素时,编译器会进行错误检查,以防加入错误的类型元素。其他方法道理大致同上。

所以,泛型机制给程序带来了更高的可读性与安全性。

泛型的简单使用

泛型类

public class Generic<T> {
    private T key;

    public Generic(T key) {
        this.key = key;
    }

    public T getKey() {
        return key;
    }

    public void setKey(T key) {
        this.key = key;
    }
}
  • 在Java库中,使用变量E表示集合的元素类型,K和V分别表示表的关键字与值的类型,T(需要时还可以用临近的字母U和S)表示任意具体的一个类型(类比ArrayList<E>,这个写法和ArrayList<T>实际上是一个意思,表示需要我们在实例化时传入一个确定的类型参数),?为通配符,表示不确定的类型(在泛型的通配符中会讲解?的用法)。实际上使用A-Z任何字母表示泛型都可以,但是一般遵守上述规则。

  • Generic引入一个类型变量T,用<>括起来,并放在类名的后面。

    当我们这样实例化Generic时:Generic<String> generic = new Generic<>()Generic<String> generic = new Generic<String>()(Java SE 7之后,构造函数可以省略泛型)、Generic<?> generic = new Generic<>("string"),实际得到如下所示的类与方法

    public class Generic<String> {
        private String key;
    
        public Generic(String key) {
            this.key = key;
        }
    
        public String getKey() {
            return key;
        }
    
        public void setKey(String key) {
            this.key = key;
        }
    }
    
  • 泛型类可以有多个类型变量。

    public class Generic<T,E> {
        private T key1;
        private E key2;
    
        public Generic(T key1, E key2) {
            this.key1 = key1;
            this.key2 = key2;
        }
    
        public T getKey1() {
            return key1;
        }
    
        public void setKey1(T key1) {
            this.key1 = key1;
        }
    
        public E getKey2() {
            return key2;
        }
    
        public void setKey2(E key2) {
            this.key2 = key2;
        }
    }
    

    初始化时,可以为T和E传入所需的不同或相同的类型参数,其方法参数及返回值也会被自动翻译为对应类型。

泛型方法

public class GenericTest {
    
    public static void main(String[] args) {
        testGeneric("str");
        testGeneric(123);
        testGeneric(1.0);
        testGeneric(false);
    }
    
   	public static <T> void testGeneric(T argement){
        System.out.println(argement.getClass());
    }

}
  • 泛型方法可以定义在普通类中也可以定义在泛型类中
  • 泛型方法的定义中,类型变量放在修饰符(这里是public static)的后面,返回参数的前面。如testGeneric(T argement)

泛型接口

这里就不自己举例了,最典型的一个泛型接口之一就是Comparable接口。

public interface Comparable<T> {
    public int compareTo(T o);
}
class User implements Comparable<User>{

    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int compareTo(User o) {
        if( name.equals(o.name))
            return age-o.age;
        return name.compareTo(o.name);
    }
}

类型擦除

概念

编译器在编译泛型的类型信息时,会将类型变量清除,并替换为限定类型。如果未进行限定(即使用T或?作为类型变量),则使用Object进行替换。若限定了类型(即使用了extends或者super限定泛型),则使用限定范围的最小类进行替换。

例如上文Generic的原始类型应该为:

public class Generic {
    private Object key;

    public Generic(Object key) {
        this.key = key;
    }

    public Object getKey() {
        return key;
    }

    public void setKey(Object key) {
        this.key = key;
    }
}

限定类型的类型擦除如下(图源Java核心技术):

image-20210308200839502

举例说明

举两个例子说一下类型擦除。

Demo1

@Test
public void testClear() {
    ArrayList<Integer> integers = new ArrayList<>();
    ArrayList<String> strings = new ArrayList<>();
    System.out.println(integers.getClass());
    System.out.println(strings.getClass());
    System.out.println(integers.getClass() == strings.getClass());
}

输出结果:

image-20210308201344719

Demo2

@Test
public void testT() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    List<Integer> list = new ArrayList<>();
    list.add(12);
    Class<? extends List> clazz = list.getClass();
    Method add = clazz.getDeclaredMethod("add",Object.class);
    //通过反射可以添加成功
    add.invoke(list, "kl");
    System.out.println(list);

}

运行这个测试方法,会输出[12,"kl"]。若是编译后泛型类型信息依然存在,即使使用反射,这里也不应该成功。

类型变量的限定和通配符的限定

引入

下面以一个例子说明引入类型变量的限定的概念

public class WildcardTest {
    public static void main(String[] args) {
        ArrayList<Employee> employees = new ArrayList<>();
        ArrayList<Manger> mangers = new ArrayList<>();
        Employee employee1 = new Employee("张三", 18, false);
        Employee employee2 = new Employee("李四", 18, true);
        Employee employee3 = new Employee("王五", 18, true);
        Manger manager1 = new Manger("小王", 22, false, 0.1);
        Manger manager2 = new Manger("小李", 22, false, 0.1);
        Manger manager3 = new Manger("小掌", 22, false, 0.1);
        employees.add(employee1);
        employees.add(employee2);
        employees.add(employee3);
        mangers.add(manager1);
        mangers.add(manager2);
        mangers.add(manager3);
        mangers.get(1);
        //使用addAll将mangers的全部元素加入employees
        employees.addAll(mangers);
        System.out.println(employees);
        //编译不通过,因为Employee不是Manger的子类
        //mangers.addAll(employee1);
    }
}

class Employee {
    private String name;
    private int age;
    private boolean sex;

    public Employee(String name, int age, boolean sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", sex=" + sex +
                '}';
    }
}
class Manger extends Employee{
    private double Commission;

    public Manger(String name, int age, boolean sex, double commission) {
        super(name, age, sex);
        Commission = commission;
    }

    public Manger(String name, int age, boolean sex) {
        super(name, age, sex);
    }

    @Override
    public String toString() {
        return "Manger{" +
                "Commission=" + Commission +
                '}';
    }
}
  • mangers.addAll(employee1),编译器会显示如下所示的错误。

    image-20210308152420622

    jdk1.8中ArrayList.addAll()的源码如下:

        public boolean addAll(Collection<? extends E> c) {
            Object[] a = c.toArray();
            int numNew = a.length;
            ensureCapacityInternal(size + numNew);  // Increments modCount
            System.arraycopy(a, 0, elementData, size, numNew);
            size += numNew;
            return numNew != 0;
        }
    

    addAll()的参数c,限定类型为Collection<? extends E>,表示Collection的类型应为E的子类型或E本身,其中E为声明数组列表时所指定的类型。

类型变量限定的“关键字”

类型变量限定的“关键字”有两个:extends和super

extends

在类型参数中使用T extends E,表示T需要是E的子类或其自身。

super(超类限定)

在类型参数中使用? super E,表示通配符?表示的类型需要是E的父类或其自身。需要注意这里为什么不能使用T super E,因为类型参数不支持超类限定。

多重限定

可以使用&表示多重限定。语法为T extends E&K...,表示T需要是E和K的子类。需要注意,通配符不支持多重限定,所以不能写作T extends E&K...

T和?的区别

在前文中我们提到,?为通配符,表示不确定的类型。那么T和?区别是什么呢?我的认为区别如下。(没有依据,如有错误,请大神指教。)

T表示任意具体的一个类型,在泛型类实例化时,泛型方法被调用时,泛型接口实现类实例化时,此时的T实际上已经被具体类型所取代。在当前对象下(就是当前实例化对象,或调用泛型方法的当前对象,或当前实现泛型接口实现类的实例化对象),T只能表示一种类型了。例如ArrayList<E>,在声明了ArrayList<String> strings = new ArrayList<>()后,strings表示的ArrayList中,E只能为String。而对于通配符?,参考前文所举的Manger和Employee例子中的addAll(),在employees.addAll()中,除了mangers,我还可以传入任意类型为Employee的子类的ArrayList。例如:

public class WildcardTest {
    public static void main(String[] args) {
        ArrayList<Employee> employees = new ArrayList<>();
        ArrayList<Manger> mangers = new ArrayList<>();
        ArrayList<Coder> coders = new ArrayList<>();
        Employee employee1 = new Employee("张三", 18, false);
        employees.add(employee1);
        Coder coder = ...;
        coders.add(coder);
        //使用addAll将mangers的全部元素加入employees
        employees.addAll(mangers);
        employees.addAll(coders);
    }
}

class Coder extends Employee{
    ...
}

employees.addAll(coders)的操作会成功,因为Coder是Employee的子类。根据addAll()的源码,在employees下,当前?对应的类型就为Coder和Manger,需要的时候可以是更多类型,所以说?表示不确定的类型。

参考链接

posted @ 2021-03-08 21:24  snail-coder  阅读(87)  评论(0)    收藏  举报