Java笔记之泛型(上)

来源:互联网 发布:阿里云费用提现 编辑:程序博客网 时间:2024/06/14 09:51

1.泛型入门

Java集合有个缺点——把一个对象“丢进”集合里之后,集合就会“忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译类型就变成了Object类型(其运行时类型没变)。

1.1编译时不检查类型的异常

public class ListErr {    public static void main(String[] args)     {        //创建一个只想保存字符串的List集合        List strList = new ArrayList();        strList.add("test1");        strList.add("test2");        //若把一个Integer对象“丢进”集合        //将引发ClassCastException异常        strList.add(5);        strList.forEach(str -> System.out.println(((String)str).length()));    }}

1.2使用泛型

从Java5之后,Java引入了“参数化类型”的概念,允许程序在创建集合时指定集合元素的类型。Java的参数化类型被称为泛型(Generic)。
使用泛型改进上一个程序:

public class GenericList {    public static void main(String[] args)     {        //创建一个只想保存字符串的List集合        List<String> strList = new ArrayList<String>();        strList.add("test1");        strList.add("test2");        //下面代码将引起编译错误        strList.add(5);        strList.forEach(str -> System.out.println(str.length()));    }}

1.3Java7泛型的“菱形”语法

从Java7开始,Java允许在构造器后不需要带完整的泛型信息,只要给出一对尖括号(<>)即可,Java可以推断尖括号里应该是什么泛型信息。

public class DiamondTest {    public static void main(String[] args)     {        //Java自动推断出ArrayList的<>里应该是String        List<String> books = new ArrayList<>();        books.add("test1");        books.add("test2");        books.forEach(ele -> System.out.println(ele.length()));        //Java自动推断出HashMap的<>里应该是String,List<String>        Map<String,List<String>> schoolsInfo = new HashMap<>();        List<String> schools = new ArrayList<>();        schools.add("test3");        schools.add("test4");        schoolsInfo.put("hello",schools);        schoolsInfo.forEach((key,value) -> System.out.println(key + "-->" + value));    }}

2.1定义泛型接口、类

包含泛型声明的类型可以在定义变量、创建对象时传入一个类型实参,从而可以动态的生成无数多个逻辑上的子类,但这种子类在物理上并不存在。
可以为任何类、接口增加泛型声明。

//定义Apple类时使用了泛型声明public class Apple<T> {    //使用T类型形参定义实例变量    private T info;    public Apple(){}    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.55);        System.out.println(a2.getInfo());    }}

注意:当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明。

2.2从泛型类派生子类

当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类派生子类,但当使用这些接口、父类时不能再包含类型形参。例如,下面代码就是错误的。

//定义类A继承Apple类,Apple类不能再跟类型形参public class A extends Apple<T>{ }

如果想从Apple类派生一个子类,由如下两种方式:

//使用Apple类时为T形参传入String类型public class A extends Apple<String>//使用Apple类时,没有为T形参传入实际的类型参数public class A extends Apple

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

public class A1 extends Apple<String> {    //正确重写了父类的方法,返回值    //与父类Apple<String>的返回值完全相同    public String getInfo()    {        return "子类" + super.getInfo();    }    //下面方法时错误的,重写父类方法时返回值类型不一致    public Object getInfo()    {        return "子类";    }}

如果使用Apple类时没有传入实际的类型参数,Java编译器会发出泛型检查警告。

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

2.3并不存在泛型类

//分别创建List<String>对象和List<Integer>对象List<String> l1 = new ArrayList<>();List<Integer> l2 = newArrayList<>();System.out.println(l1.getClass() == l2.getClass());

运行上面代码将输出true,因为不管泛型的实际类型参数是什么,它们在运行时总有同样的类。
因此在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。

public class R<T> {    //下面代码错误,不能在静态变量声明中使用类型形参    Static T info;    T age;    public void foo(T msg){}    //下面代码错误,不能在静态方法中中使用类型形参    public static void bar(T msg){}}

由于系统中并不会真正生成泛型类,所以instanceof运算符不能使用泛型类。

3类型通配符

public void test(List<Object> c){    for(int i = 0;i < c.size() ; ++i)    {        System.out.println(c.get(i));    }}//创建一个List<String>对象List<String> strList = new ArrayList<>();//调用前面的方法test(strList);

上面的程序将出现编译错误,表明List<String>对象不能被当成List<Object>对象使用,即List<String>类并不是List<Object>的子类。
注意:如果Foo时Bar的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G并不是G的子类型!

与数组进行对比,先看一下数组是如何工作的。

public class ArrayErr {    public static void main(String[] args)     {        Integer[] ia = new Integer[5];        //可以把一个Integer[]数组赋给Number[]变量        Number[] na = ia;        //下面代码编译正常,但运行时会引发ArrayStoreException异常        //因为0.5不是Integer        na[0] = 0.5;    }}

在Java的早期设计中,允许Integer[]数组赋值给Number[]变量存在缺陷,因此Java在泛型设计时进行了改进,不再允许把Integer[]数组赋值给Number[]变量。如下代码将会导致编译错误。

List<Integer> iList = new ArrayList<>();//下面代码导致编译错误List<Number> nList = iList;

注意:数组和泛型有所不同,假设Foo是Bar的一个子类型,那么Foo[]依然是Bar[]的子类型;但G不是G的子类型。

3.1使用类型通配符

为了表示各种泛型List的父类,可以使用类型通配符,类型通配符是一个问号,它的元素类型可以匹配任何类型。如下所示:

public void test(List<?> c){    for(int i = 0;i < c.size() ; ++i)    {        System.out.println(c.get(i));    }}

现在使用任何类型的List来调用它,程序依然可以访问集合c中的元素,其类型是Object。
这种写法可以适应于任何支持泛型声明的接口和类。
但这种带通配符的List仅表示它是各种泛型List的父类,并不能把元素加入其中。

List<?> c = new ArrayList<String>();//下面程序引起编译错误c.add(new Object());

因为程序无法确定c集合中元素的类型,所以不能向其中添加对象。

3.2设定类型通配符的上限

List

//定义一个抽象类Shapepublic abstract class Shape {    public abstract void draw(Canvas c);}//定义Shape的子类Circlepublic class Circle extends Shape {    public abstract void draw(Canvas c)    {        System.out.println("在画布" + c + "上画一个圆");    }}//定义Shape的子类Rectanglepublic class Rectangle extends Shape {    public abstract void draw(Canvas c)    {        System.out.println("把一个矩形画在画布" + c + "上");    }}

上面定义了三个形状类。接下来定义一个Canvas类,该画布类可以画数量不等的形状。

public class Canvas {    public void drawAll(List<Shape> shapes)    {        for(Shape s : shapes)        {            s.draw(this);        }    }}

此时List<Circle>并不是List<Shape>的子类型,所以将引起编译错误。改写为如下形式:

public class Canvas {    public void drawAll(List<?> shapes)    {        for(Object obj : shapes)        {            Shape s = (Shape)obj;            s.draw(this);        }    }}

这种方式虽然没错,但极为繁琐。我们需要一种泛型表示方法,可以表示所有Shape泛型List的父类。Java泛型提供了被限制的泛型通配符。

//它表示所有Shape泛型List的父类List<? extends Shape>

有了这种被限制的泛型通配符,我们就可以把上面程序改写为如下形式:

public class Canvas {    public void drawAll(List<?extends Shape> shapes)    {        for(Shape s : shapes)        {            s.draw(this);        }    }}

此处的问号代表一个未知的类型,但是此处的这个未知类型一定是Shape的子类型(也可以是Shape本身),因此把Shape称为这个通配符的上限。
类似地,由于程序无法确定这个受限制的通配符的具体类型,所以不能把Shape对象或其子类的对象加入这个泛型集合中。

public void addRectangle(List<? extends Shape> shapes){    //下面代码引起编译错误    shapes.add(0,new Rectangle());}

3.3设定类型形参的上线

Java泛型不仅允许在使用通配符形参时设定上限,而且可以在定义类型形参时设定上限,用于表示传给该类型形参的实际类型要么是该上限类型,要么是该上限类型的子类。

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不是Number的子类型,所以引起编译错误        Apple<String> as = new Apple<>();    }}

在一种更极端的情况下,程序需要为类型形参设定多个上限(最多一个父类上限,可以有多个接口上限),表明该类型形参必须是其父类的子类(父类本身也行),并且实现多个上限接口。

public class Apple<T extends Number & java.io.Serializable>{    ...}

所有的接口上限必须位于类上限之后。

原创粉丝点击