8. 泛型

写在之前

以下是《疯狂Java讲义》中的一些知识,如有错误,烦请指正。


泛型初衷

  • 集合对元素类型没有任何限制,这样可能引发一些问题:例如想创建一个只能保存Dog对象的集合,但程序也可以轻易地将Cat对象“丢”进去,所以可能引发异常。
  • 由于把对象“丢进”集合时,集合丢失了对象的状态信息,集合只知道它盛装的是Object,因此取出集合元素后通常还需要进行强制类型转换。这种强制类型转换既会增加编程的复杂度、也可能引发ClassCastException

泛型就是允许在定义类、接口指定类型形参,这个类型形参在将在声明变量、创建对象时确定(即传入实际的类型参数,也可称为类型实参)。

import java.util.*;
public class GenericList
{
    public static void main(String[] args)
    {
        // 创建一个只想保存字符串的List集合
        List<String> strList = new ArrayList<String>();  // 构造器后不需要带完整的泛型信息
        strList.add("疯狂Java讲义");
        strList.add("疯狂Android讲义");
        // 下面代码将引起编译错误
        strList.add(5);    // ②
        strList.forEach(str -> System.out.println(str.length())); // ③
    }
}

可以为任何类、接口添加泛型声明。

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());
        // 由于传给T形参的是Double,所以构造器参数只能是Double或double
        Apple<Double> a2 = new Apple<>(5.67);
        System.out.println(a2.getInfo());
    }
}

泛型类派生子类

当创建带泛型声明的接口、父类之后,可以为该接口创建实现类或派生子类。注意:使用这些接口、父类时,不能再包含类型形参。应该为类型形参传入实际的类型。使用类、接口时也可以不为类型形参传入实际的类型参数。

public class A2 extends Apple
{
    // 重写父类的方法
    public String getInfo()
    {
        // super.getInfo()方法返回值是Object类型,
        // 所以加toString()才返回String类型
        return super.getInfo().toString();
    }
}

并不存在泛型类
虽然可以把ArrayList<String>类当成ArrayList的子类,事实上ArrayList<String>类也确实是一种特殊的ArrayList类,这个ArrayList<String>对象只能添加String对象作为集合元素。但实际上,系统并没有为ArrayList<String>生成新的class文件,而且也不会把ArrayList<String>当成新类来处理。

系统中不会真正生成泛型类,所以instanceof运算符后不能使用泛型类,也不能在静态变量声明中使用类型形参

类型通配符

如果FOO是Bar的一个子类型(子类或者子接口),而G是具有泛型声明的类或者接口,G<Foo>并不是G<Bar>的子类型。数组和泛型不同,假设Foo是Bar的一个子类型,那么Foo[]依然是Bar[]的子类型。
List<String>对象不能被当成List<Object>对象使用,也就是说:List<String>类并不是List<Object>类的子类。

使用类型通配符
为了表示各种泛型List的父类,我们需要使用类型通配符,类型通配符是一个问号(?),将一个问号作为类型实参传给List集合,写作:List<?>(意思是未知类型元素的List)。这个问号(?)被称为通配符,它的元素类型可以匹配任何类型

类型通配符的上限
使用List<?>这种形式是,即表明这个List集合可以是任何泛型List的父类。但还有一种特殊的情形,我们不想这个List<?>是任何泛型List的父类,只想表示它是某一类泛型List的父类。

//Shape类型的子类型,Shape称为通配符的上限
List<? extends Shape>

import java.util.*;
public class Canvas
{
//  // 同时在画布上绘制多个形状
//  public void drawAll(List<Shape> shapes)
//  {
//      for (Shape s : shapes)
//      {
//          s.draw(this);
//      }
//  }
// 比较臃肿
//  public void drawAll(List<?> shapes)
//  {
//      for (Object obj : shapes)
//      {
//          Shape s = (Shape)obj;
//          s.draw(this);
//      }
//  }
    // 同时在画布上绘制多个形状,使用被限制的泛型通配符
    public void drawAll(List<? extends Shape> shapes)
    {
        for (Shape s : shapes)
        {
            s.draw(this);
        }
    }

    public static void main(String[] args)
    {
        List<Circle> circleList = new ArrayList<Circle>();
        Canvas c = new Canvas();
        // 由于List<Circle>并不是List<Shape>的子类型,
        // 所以下面代码引发编译错误
        c.drawAll(circleList);
    }
}

类型形参的上限
Java泛型不仅允许在使用通配符形参时设定类型上限,也可以在定义类型形参时设定上限,用于表示创给该类型形参的实际类型必须是该上限类型,或是该上限类型的子类。例如: Apple<T extends Number>

public class Apple<T extends Number>
{
    T col;
    public static void main(String[] args)
    {
        Apple<Integer> ai = new Apple<>();
        Apple<Double> ad = new Apple<>();
        // 下面代码将引起编译异常,下面代码试图把String类型传给T形参
        // 但String不是Number的子类型,所以引发编译错误
        Apple<String> as = new Apple<>();
    }
}

泛型方法

如果定义类、接口是没有使用类型形参,但定义方法时想自己定义类型形参,这也是可以的。泛型方法的方法签名比普通方法的方法签名多了类型形参声明。

修饰符 <T, S> 返回值类型 方法名(形参列表)
{
}

下面看一个只定义了一个T类型形参的泛型方法

import java.util.*;
public class GenericMethodTest
{
    // 声明一个泛型方法,该泛型方法中带一个T类型形参,
    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);
    }
}

为了让编译器能准确判断,不要制造迷惑。来看一个定义了两个类型形参的错误例子

import java.util.*;
public class ErrorTest
{
    // 声明一个泛型方法,该泛型方法中带一个T类型形参
    static <T> void test(Collection<T> from, Collection<T> to)
    {
        for (T ele : from)
        {
            to.add(ele);
        }
    }
    public static void main(String[] args)
    {
        List<Object> as = new ArrayList<>();
        List<String> ao = new ArrayList<>();
        // 下面代码将产生编译错误
        test(as , ao);
    }
}

上面的程序面临选择时就无法识别T所代表的实际类型。

泛型方法和通配符的区别
大多数时候都可以使用泛型方法代替类型通配符。
使用通配符:在不同的调用点传入不同的实际类型;
泛型方法:允许类型形参被用来表示方法的一个或多个参数之间的类型依赖关系。
类型通配符既可以在方法签名中定义形参的类型,也可以用于定义变量的类型;但泛型方法中的类型形参必须在对应方法中显示声明。

菱形语法与泛型构造器

class Foo
{
    public <T> Foo(T t)
    {
        System.out.println(t);
    }
}
public class GenericConstructor
{
    public static void main(String[] args)
    {
        // 泛型构造器中的T参数为String。
        new Foo("疯狂Java讲义");
        // 泛型构造器中的T参数为Integer。
        new Foo(200);
        // 显式指定泛型构造器中的T参数为String,
        // 传给Foo构造器的实参也是String对象,完全正确。
        new <String> Foo("疯狂Android讲义");
        // 显式指定泛型构造器中的T参数为String,
        // 但传给Foo构造器的实参是Double对象,下面代码出错
        new <String> Foo(12.3);
    }
}

菱形语法允许调用构造器时在构造器后使用一对尖括号来代表泛型信息。但如果程序显示制定了泛型构造器中声明的类型形参的实际类型,则不可使用菱形语法(只使用尖括号)。

class MyClass<E>
{
    public <T> MyClass(T t)
    {
        System.out.println("t参数的值为:" + t);
    }
}
public class GenericDiamondTest
{
    public static void main(String[] args)
    {
        // MyClass类声明中的E形参是String类型。
        // 泛型构造器中声明的T形参是Integer类型
        MyClass<String> mc1 = new MyClass<>(5);
        // 显式指定泛型构造器中声明的T形参是Integer类型,
        MyClass<String> mc2 = new <Integer> MyClass<String>(5);
        // MyClass类声明中的E形参是String类型。
        // 如果显式指定泛型构造器中声明的T形参是Integer类型
        // 此时就不能使用"菱形"语法,下面代码是错的。
//      MyClass<String> mc3 = new <Integer> MyClass<>(5);
    }
}

设定通配符下限
Java集合框架中的TreeSet<E>有一个构造器也用到了这种设定通配符下限的语法,如下所示:
TreeSet(Comparator<? super E> c)

<? super Type>表示它必须是Type本身或者其父类。

import java.util.*;
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);
    }
}
//TreeSet(Comparator<? super E> c)
import java.util.*;
public class TreeSetTest
{
    public static void main(String[] args)
    {
        // Comparator的实际类型是TreeSet的元素类型的父类,满足要求
        TreeSet<String> ts1 = new TreeSet<>(
            new Comparator<Object>()
        {
            public int compare(Object fst, Object snd)
            {
                return hashCode() > snd.hashCode() ? 1
                    : hashCode() < snd.hashCode() ? -1 : 0;
            }
        });
        ts1.add("hello");
        ts1.add("wa");
        // Comparator的实际类型是TreeSet元素的类型,满足要求
        TreeSet<String> ts2 = new TreeSet<>(
            new Comparator<String>()
        {
            public int compare(String first, String second)
            {
                return first.length() > second.length() ? -1
                    : first.length() < second.length() ? 1 : 0;
            }
        });
        ts2.add("hello");
        ts2.add("wa");
        System.out.println(ts1);
        System.out.println(ts2);
    }
}

Java8改进的类型推断

  • 可调用方法的上下文推断类型参数的目标类型
  • 可在方法的调用链中,将推断得到的类型参数传递到最后一个方法。
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());
        // 无需使用下面语句在调用nil()方法时指定类型参数的类型
        MyUtil.cons(42, MyUtil.<Integer>nil());

        // 希望系统能推断出调用nil()方法类型参数为String类型,
        // 但实际上Java 8依然推断不出来,所以下面代码报错
//      String s = MyUtil.nil().head();
        String s = MyUtil.<String>nil().head();
    }
}

擦除和转换

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

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

class Apple<T extends Number>
{
    T size;
    public Apple()
    {
    }
    public Apple(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)
    {
        Apple<Integer> a = new Apple<>(6);
        // a的getSize方法返回Integer对象
        Integer as = a.getSize();
        // 把a对象赋给Apple变量,丢失尖括号里的类型信息
        Apple b = a; 
        // b只知道size的类型是Number
        Number size1 = b.getSize();
        // 下面代码引起编译错误
        Integer size2 = b.getSize();  
    }
}
import java.util.*;
public class ErasureTest2
{
    public static void main(String[] args)
    {
        List<Integer> li = new ArrayList<>();
        li.add(6);
        li.add(9);
        List list = li;
        // 下面代码引起“未经检查的转换”的警告,编译、运行时完全正常
        List<String> ls = list;     // ①
        // 但只要访问ls里的元素,如下面代码将引起运行时异常。
        System.out.println(ls.get(0));
    }
}

泛型与数组

数组元素的类型不能包含类型变量或者类型形参。

List<?> lsa = new ArrayList<?>[10];
Object[] oa = lsa;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li;
Object target = lsa[1].get(0);
if (target instanceof String){
String s = (String) target;
}

类似的,创建元素类型是类型变量的数组对象也将导致编译错误。由于类型变量在运行时并不存在,而编译器无法确定实际类型是什么。

<T> T[] makeArray(Collection<T> coll){
    //下面代码将导致编译错误
    return new T[coll.size()];
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容

  • 开发人员在使用泛型的时候,很容易根据自己的直觉而犯一些错误。比如一个方法如果接收List作为形式参数,那么如果尝试...
    时待吾阅读 990评论 0 3
  • object 变量可指向任何类的实例,这让你能够创建可对任何数据类型进程处理的类。然而,这种方法存在几个严重的问题...
    CarlDonitz阅读 882评论 0 5
  • 一、泛型简介1.引入泛型的目的 了解引入泛型的动机,就先从语法糖开始了解。 语法糖 语法糖(Syntactic S...
    Android进阶与总结阅读 1,013评论 0 9
  • 第8章 泛型 通常情况的类和函数,我们只需要使用具体的类型即可:要么是基本类型,要么是自定义的类。但是在集合类的场...
    禅与计算机程序设计艺术阅读 2,131评论 6 10
  • 突然有点失去写作的灵感,想来点灵感写点什么好? 春节过得匆匆的,时间是一去不复返的。 我依然一早去吃早餐,即使上的...
    伏魔战神阅读 292评论 5 1