第九章泛型

Java 学习笔记

第九章 泛型

泛型入门

编译时不检查类型的异常
import java.util.ArrayList;
import java.util.List;

public class ListErr {
    public static void main(String[] args)
    {
        List strList=new ArrayList();
        strList.add("lancibe");
        strList.add("xun");
        strList.add(5);
        strList.forEach(str -> System.out.println(((String)str).length()));
    }
}
  • 上面程序创建了一个LIst集合,而且只希望该集合保存字符串,但是如果不小心放入了一个Integer对象,则引发ClassCastExpection异常。
使用泛型
  • 对于前面的程序,可以使用泛型来改进该程序:

    import java.util.ArrayList;
    import java.util.List;
    
    public class GenericList {
        public static void main(String args[])
        {
            //创建一个只想保存字符串的List集合
            List<String> strList = new ArrayList<>();
            strList.add("lancibe");
            strList.add("xun");
            //下面代码直接引起编译错误
            strList.add(5);
            strList.forEach(str -> System.out.println(str.length()));
        }
    }
    
    
  • 此外,在最后一行输出的时候,不需要进行强制类型转换,因为strList对象可以“记住”他的所有集合元素都是String类型。

Java 9 增强的“菱形”语法
  • 在Java 7 以前,如果使用带有泛型的接口、类定义变量,那么调用构造器创建对象时构造器的后面也必须带泛型,这显得会有些多余。例如下面两条语句

    List<String> strList = new ArrayList<String>();
    Map<String, Integer> scores = new HashMap<String, Integer>();
    
  • 上面两条语句后面的尖括号里的内容完全是多余的。故应该改写为如下格式

    List<String> strList = new ArrayList<>();
    Map<String, Integer> scores = new HashMap<>();
    
  • 这种语法也被称为“菱形”语法,下面程序示范了Java 7 的菱形语法。

    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    public class DiamondTest {
        public static void main(String[] args)
        {
            //java自动推断出ArrayList的<>里应该是String
            List<String> books = new ArrayList<>();
            books.add("lancibe");
            books.add("xun");
            //遍历
            books.forEach(str -> System.out.println(str.length()));
    
            //使用map,结果一样
            Map<String, List<String>> schoolsInfo = new HashMap<>();
            List<String> schools = new ArrayList<>();
            schools.add("123");
            schools.add("456");
            schoolsInfo.put("xx", schools);
            schoolsInfo.forEach((key, value) -> System.out.println(key+"-->"+value));
        }
    }
    
  • 程序运行结果如下:

    lancibe@lancibe-PC:~/java/test/src/chapte9/src$ java DiamondTest 
    7
    3
    xx-->[123, 456]
    
  • Java 9 再次增强了“菱形”语法,他甚至允许在创建匿名内部类时使用菱形语法, Java可以根据上下文来推断匿名内部类中泛型的类型:

    interface Foo<T>
    {
        void test(T t);
    }
    
    public class AnnoymousDiamond {
        public static void main(String[] args)
        {
            //制定Foo类中泛型为String
            Foo<String> f = new Foo<String>() {
                //必须要进行方法的重载,test方法的参数类型为String
                @Override
                public void test(String s) {
                    System.out.println("test方法的s参数为:"+s);
                }
            };
    
            //使用泛型通配符,此时相当于通配符的上限为Object
            Foo<?> fo = new Foo<Object>() {
                @Override
                public void test(Object o) {
                    System.out.println("test方法的o参数为"+o);
                }
            };
    
            //使用泛型通配符,此时通配符上限为Number
            Foo<? extends Number> fn = new Foo<Number>() {
                @Override
                public void test(Number number) {
                    System.out.println("test方法的Number参数为:"+number);
                }
            };
        }
    }
    

深入泛型

  • 所谓泛型,就是允许在定义类、接口、方法时使用类型形参、这个类型形参(或叫泛型)将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。Java 5 改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参,这就是前面程序中看到的List<String>和ArrayList<String>两种类型。
定义泛型接口、类
  • 下面是Java 5 改写后List接口、Iterator接口、Map的代码片段。

    public interface List<E>
    {
        void add<E x>;
        Iterator<E> iterator();
        ...
    }
    public interface Iterator<E>
    {
        E next();
        boolean hasNext();
        ...
    }
    public interface Map<K, V>
    {
        Set<K> keySet()
        V put(K key, V value)
        ...
    }
    
  • 可以为任何类、接口增加泛型声明(并不是只有集合类才可以使用,但是集合类确实是泛型使用的重要场所)。

    public class Apple<T> {
        //使用T类型定义实例变量
        private T info;
        public Apple(){}
        //下面方法中使用T类型来定义构造器
        public Apple(T info)
        {
            this.info = info;
        }
    
        public void setInfo(T info) {
            this.info = info;
        }
    
        public T getInfo() {
            return this.info;
        }
    
        public static void main(String[] args)
        {
            //由于传给T形参的是String, 所以构造器参数只能是String
            Apple<String> a1 = new Apple<>("苹果");
            System.out.println(a1.getInfo());
            //同理
            Apple<Double> a2 = new Apple<>(5.67);
            System.out.println(a2.getInfo());
        }
    }
    
从泛型类派生子类
  • 当创建了带泛型声明的接口、父类时,可以为该接口创建实现类、或从该父类派生子类,需要指出的是,**当使用这些接口、父类时不能再包含泛型形参。**例如下面代码就是错误的

    public class A extends Apple<T>{}
    
  • 方法中的形参代表变量、常亮、表达式等数据,本书把它们直接称为形参,或者称为数据形参。定义方法时可以声明数据形参、调用方法(使用方法)时必须为这些数据形参传入实际的数据;与此类似的是,定义类、接口、方法时可以声明泛型形参,使用类、接口、方法时应该为泛型形参传入实际的类型。

  • 如果想从Apple类派生一个子类,则可以改为如下代码:

    public class A extends Apple<String>
    
  • 调用方法时必须为所有数据形参传入参数值,与调用方法不同的是,使用类、接口时也可以不为泛型形参传入实际的类型参数,

    public class A extends Apple
    
  • 像这种使用Apple类时省略泛型的形式被称为原始类型(raw type)

  • 如果从Apple<String>类派生子类,则在Apple类中所有使用T类型的地方都将被替换成String类型,即他的子类将会继承到String getInfo()和void setInfo(String info)两个方法,如果子类需要重写父类的方法,就必须注意到这点。

    public class A1 extends Apple<String>{
        @Override
        public String getInfo() {
            return "子类"+super.getInfo();
        }
    }
    
  • 如果使用Apple类时没有传入实际的类型(即使用原始类型),Java编译器可能会发出警告:使用了未经检查或不安全的操作——这就是泛型检查的警告。如果希望看到该警告提示的更详细的信息,则可以通过javac命令增加-Xlint:unchecked选项来实现。此时,系统会把Apple<T>类里的T形参当成Object来处理。

    public class A2 extends Apple {
        @Override
        public String getInfo() {
            //super.getIndo()是Object类型
            //所以加toString才是String类型
            return super.getInfo().toString();
        }
    }
    
并不存在泛型类
List<String> l1 = new ArrayList<>();
List<Integer> l2 = new ArrayList<>();
System.out.println(l1.getClass() == l2.getClass());
  • 上面的代码片段输出true

类型通配符

  • 正如前面的,当使用一个泛型类时(包括声明变量和创建对象两种情况),都应该为这个泛型类传入一个类型实参。如果没有传入类型实际参数,编译器就会提出泛型警告。假设现在需要定义一个方法,该方法里有一个集合形参,集合形参的元素类型是不确定的,那应该怎样定义呢。

    public void test(List c)
    {
        for (int i = 0 ; i < c.size() ; i++)
        {
            System.out.println(c.get(i));
        }
    }
    
  • 上面的代码没有问题,但是List接口是需要类型参数的,因此会引起泛型警告。应使用下面方法

    public void test(List<Object> c)
    {
        for (int i = 0 ; i < c.size() ; i++)
        {
            System.out.println(c.get(i));
        }
    }
    
  • 与数组进行对比,先看数组是如何工作的,在数组中,程序可以直接把一个Integer[]数组赋给一个Number[]变量。如果试图把一个Double对象保存在该Number[]数组中,编译可以通过,但是在运行时跑出ArrayStoreException异常。

    public class ArrayErr {
        public static void main(String[] args)
        {
            Integer[] ia = new Integer[5];
            Number[] na = ia;
            na[0] = 0.5;
        }
    }
    
  • 上面程序会在运行时异常。

使用类型通配符
  • 为了表示各种泛型List的父类,可以使用类型通配符,类型通配符是一个问号,表示将一个问号作为类型实参传给List集合,写作:List<?>(意思是元素类型未知的List)。这个问号被称为通配符,他的元素类型可以匹配任何类型。可以将上面方法改写为如下格式:

    public void test(List<?> c)
    {
        for (int i = 0 ; i < c.size() ; i++)
        {
            System.out.println(c.get(i));
        }
    }
    
  • 现在任何类型的List来调用它程序依然可以访问c中的元素,其类型是Object,这永远是安全的,因为不管List的真是类型是什么,它包含的都是Object

  • 但这种带通配符的List仅表示他是各种泛型List的父类,并不能把元素加入其中,否则代码将会引起错误。

    List<?> c = new ArrayList<String>();
    //下面代码将引起编译错误
    c.add(new Object());
    
设定类型通配符的上限
  • 下面先定义三个形状类

    import java.awt.*;
    
    public abstract class Shape {
        public abstract void draw(Canvas c);
    }
    
    import java.awt.*;
    
    public class Circle extends Shape{
        //方法实现
        @Override
        public void draw(Canvas c) {
            System.out.println("在画布"+c+"上画一个圆");
        }
    }
    
    import java.awt.*;
    
    public class Rectangle extends Shape {
        @Override
        public void draw(Canvas c) {
            System.out.println("把一个矩形画在画布"+c+"上");
        }
    }
    
  • Shape是一个抽象父类,有两个子类:Circle和Rectangle。接下来定义一个Canvas类,该画布类可以画数量不等的形状(Shape子类的对象),那应该如何定义这个Canvas类呢。考虑如下的Canvas实现类

    import java.util.List;
    
    public class Canvas {
        //同时在画布上绘制多个形状
        public void drawAll(List<Shape> shapes)
        {
            for (Shape s : shapes)
            {
                s.draw(this);
            }
        }
    }
    
  • 这将会引起编译错误。关键在于List<Circle>并不是List<Shape>的子类型,所以不能把前者对象当成后者使用。被限制的泛型通配符如下:

    List<? extends Shape>
    
  • 有了这种被限制的泛型通配符,就可以把上面的Canvas程序改为如下形式

    import org.jetbrains.annotations.NotNull;
    import java.util.List;
    
    public class Canvas {
        //同时在画布上绘制多个形状
        public void drawAll(@NotNull List<? extends Shape> shapes)
        {
            for (Shape s : shapes)
            {
                s.draw(this);
            }
        }
    }
    
  • 对于更广泛的泛型来说,指定通配符上限就是为了支持类型形变。比如Foo是Bar的子类,A<Bar>就相当于A<? extends Foo>的子类,可以将A<Bar>赋值给A<? extends Foo>类型的变量,这种型变方式被称为协变。

  • 对于协变的泛型类来说,它只能调用泛型类型作为返回值类型的方法(编译器会将该方法返回值当成通配符上限的类型),而不能调用泛型类型作为参数的方法。

设定类型通配符的下限
  • 形式为A<? super Xxx>

    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.List;
    
    public class MyUtils {
        //下面dest集合元素的类型必须与src集合元素的类型相同,或者是其父类
        public static <T> T copy(Collection<? super T> dest, Collection<T> src)
        {
            T last = null;
            for (T ele : src)
            {
                last = ele;
                //逆变的泛型集合添加元素是安全的
                dest.add(ele);
            }
            return last;
        }
        public static void main(String[] args)
        {
            List<Number> ln = new ArrayList<>();
            List<Integer> li = new ArrayList<>();
            li.add(5);
            //此处可准确的知道最后一个被复制的元素是Integer类型
            //与src集合元素的类型相同
            Integer last = copy(ln, li);
            System.out.println(ln);
        }
    }
    
  • 使用这种语句,就可以保证在最后调用后推断出最后一个被复制的元素类型是Integer,而不是笼统的Number类型。

  • 实际上,Java 集合框架中的TreeSet<E>有一个构造器也用到了这种设定通配符下限的语法,如下所示。

    TreeSet(Comparator<? super E> c)
    
  • 正如前一章介绍的,TreeSet会对集合中的元素按自然顺序或定制顺序进行排序。如果需要TreeSet对集合中的所有元素进行定制排序,则要求TreeSet对象有一个与之关联的Comparator对象。上面构造器中的参数c就是进行定制排序的Comparator对象。

  • Comparator接口也是一个带泛型声明的接口

    public interface Comparator<T>
    {
        int compare (T fst, T snd);
    }
    
  • 通过这种带下限的通配符的语法,可以在创建TreeSet对象时灵活地选择合适的Comparator。假定需要创建一个TreeSet<String>集合,并传入一个可以比较String大小的Comparator,这个Comparator既可以是Comparator<String>,也可以是Comparator<Object>——只要尖括号里传入的类型是String的父类型(或它本身)即可。

    import java.util.Comparator;
    import java.util.TreeSet;
    
    public class TreeSetTest {
        public static void main(String[] args)
        {
            //Comparator的实际类型时TreeSet的元素类型的父类,满足要求
            TreeSet<String> ts1 = new TreeSet<>(new Comparator<Object>() {
                @Override
                public int compare(Object fst, Object snd) {
                    return hashCode() > snd.hashCode() ? 1
                            : hashCode() < snd.hashCode() ? -1 : 0;
                }
            });
            ts1.add("hello");
            ts1.add("wa");
    
            TreeSet<String> ts2 = new TreeSet<>(new Comparator<String>() {
                @Override
                public int compare(String o1, String o2) {
                    return o1.length() > o2.length() ? -1
                            : o1.length() < o2.length() ? 1 : 0;
                }
            });
            ts2.add("hello");
            ts2.add("wa");
            System.out.println(ts1);
            System.out.println(ts2);
        }
    }
    
设定泛型形参的上限
  • Java 泛型不仅允许在使用通配符形参时设定上限,而且可以在定义泛型形参时设定上限,用于表示传给该泛型形参的实际类型要么是该上限类型,要么是该上限类型的子类。

    public class Apple2<T extends Number> {
        T col;
        public static void main(String[] args)
        {
            Apple2<Integer> ai = new Apple2<>();
            Apple2<Double> ad = new Apple2<>();
            //下面代码编译异常,试图把String类型传给T形参
            Apple2<String> as = new Apple2<>();
        }
    }
    

泛型方法

定义泛型方法
  • 假设需要实现这样的方法:该方法负责将一个Object数组的所有元素添加到一个Collection集合中。考虑采用如下代码实现:

    static void fromArrayToCollection(Object[] a, Collection<Object> c)
    {
        for (Object o : a)
        {
            c.add(o);
        }
    }
    
  • 上面定义的方法没有任何问题,关键在于方法中的c形参,他的数据类型是Collection<Object>。正如前面介绍的,Collection<String>不是Collection<Object>的子类型——所以这个方法的功能非常有限,他只能将Object[]数组的元素赋值到元素为Object的Collection集合中,即下面代码编译错误

    String[] strArr = {"a", "b"};
    List<String> strList = new ArrayList<>();
    fromArrayToCollection(strArr, strList);
    
  • 为解决这个问题,可以使用Java 5 提供的泛型方法(Generic Method)。所谓泛型方法,就是在声明方法时定义一个或多个泛型形参。泛型方法的语法格式如下:

    修饰符 <T, S> 返回值类型 方法名(形参列表)
    {
    	//方法体
    }
    
  • 前面的fromArrayToCollection可以改写为如下格式:

    static <T> void fromArrayToCollection (T[] a, Collection<T> c)
    {
        for (T o : a)
        {
            c.add(o);
        }
    }
    
  • 下面程序示范了完整的用法:

    import java.util.ArrayList;
    import java.util.Collection;
    
    public class GenericMethodTest {
        static <T> void fromArrayToCollection (T[] a, Collection<T> c)
        {
            for (T o : a)
            {
                c.add(o);
            }
        }
        public static void main(String[] args)
        {
            Object[] oa = new Object[100];
            Collection<Object> co = new ArrayList<>();
            //下面代码中T代表Object类型
            fromArrayToCollection(oa, co);
            String[] sa = new String[100];
            Collection<String> cs = new ArrayList<>();
            //下面代码中T代表String类型
            fromArrayToCollection(sa, cs);
            //下面代码中T代表Object类型
            fromArrayToCollection(sa, co);
            Integer[] ia = new Integer[100];
            Float[] fa = new Float[100];
            Number[] na = new Number[100];
            Collection<Number> cn = new ArrayList<>();
            //下面代码中T代表Number类型
            fromArrayToCollection(ia, cn);
            //下面代码中T代表Number类型
            fromArrayToCollection(fa, cn);
            //下面代码中T代表Number类型
            fromArrayToCollection(na, cn);
            //下面代码中T代表Object类型
            fromArrayToCollection(na, co);
            //下面代码中T代表一个String类型,但na是一个Number数组
            //因为Number既不是String类型也不是它的子类,所以会出现编译错误
            //fromArrayToCollection(na, cs);
        }
    }
    
泛型方法和类型通配符的区别
  • 大多数时候都可以使用泛型方法来代替类型通配符,也可以同时使用泛型方法和通配符
Java 7 的“菱形”语法与泛型构造器
  • 一旦定义了泛型构造器,接下来在调用构造器时,就不仅可以让Java根据数据参数的类型来“推断”泛型形参的类型,而且程序员也可以显式地为构造器中的泛型形参指定实际的类型:

    class Foo2
    {
        public <T> Foo2(T t)
        {
            System.out.println(t);
        }
    }
    public class GenericConstructor {
        public static void main(String[] args)
        {
            new Foo2("lancibe");
            new Foo2(200);
            new <String> Foo2("xun");
            new <Double> Foo2(12.3);
        }
    }
    
  • 要注意:**如果程序显式地指定了泛型构造器中声明的泛型形参的实际类型,则不可以使用“菱形”语法。**例如如下代码就是错误的

    MyClas<String> mc = new <Integer> MyClass<>(5);
    
泛型方法与方法重载
  • 形式如下:

    public static <T> void copy(Collection<T> dest, Collection<? extends T> src){...}
    public static <T> void copy(Collection<? super T? dest, Collection<T> src){...}
    
  • 比较好理解。

Java 8 改进的类型推断
  • Java 8 改进了泛型方法的类型推断能力,类型推断主要有如下两个方面

    • 可通过调用方法的上下文来推断泛型的目标类型
    • 可在方法调用链中,将推断得到的泛型传递到最后一个方法
  • 如下程序示范了Java 8 对泛型方法的类型推断

    class MyUtil<E>
    {
        public static <Z> MyUtil<Z> nil()
        {
            return null;
        }
        public static <Z> MyUtil<Z> cons(Z head, MyUtil<Z> tail)
        {
            return null;
        }
        E head()
        {
            return null;
        }
    }
    public class InferenceTest {
        public static void main(String[] args)
        {
            //通过方法赋值的目标参数推断泛型为String
            MyUtil<String> ls = MyUtil.nil();
            //无需使用下面语句在调用nil()方法时制定泛型的类型
            MyUtil<String> mu = MyUtil.<String>nil();
            //可调用cons()方法所需的参数类型来推断泛型为Integer
            MyUtil.cons(42, MyUtil.nil());
            //无需使用下面语句
            MyUtil.cons(42, MyUtil.<Integer>nil());
        }
    }
    

擦除和转换

  • 在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的Java代码保持一致,也允许在使用带泛型声明的类时不指定实际类型。如果没有为这个泛型类指定实际的类型,此时被称作raw type(原始类型),默认是声明该泛型形参时指定的第一个上限类型。

  • 当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都被扔掉。比如一个List<String>被转换为List,则该List对集合元素的类型检查变成了泛型参数上限(即Object)。下面程序示范了这种擦除:

    class Apple3<T extends Number>
    {
        T size;
        public Apple3(){}
        public Apple3(T size)
        {
            this.size = size;
        }
    
        public void setSize(T size) {
            this.size = size;
        }
    
        public T getSize() {
            return this.size;
        }
    }
    public class ErasureTest {
        public static void main(String[] args)
        {
            Apple3<Integer> a = new Apple3<>(6);
            //a的getSize()方法返回Integer对象
            Integer as = a.getSize();
            //把a对象赋给Apple3变量,丢失尖括号里的类型信息
            Apple3 b = a;
            //b只知道size的类型是Number
            Number size1 = b.getSize();
            //下面代码编译错误
            //Integer size2 = b.getSize();
        }
    }
    

泛型与数组

  • Java泛型有一个很重要的设计原则——如果一段代码在编译时没有提出未经检查的转换的警告,则运行时不会引发ClassCastException异常。正是基于这个原因,数组元素的类型不能包含泛型变量或泛型形参,除非是无上限的类型通配符。

  • Java允许创建无上限的通配符泛型数组,例如new ArrayList<?>[10]

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lanciberrr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值