java泛型

1 泛型入门

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

Java集合之所以被设计成这样,是因为设计集合的程序员不会知道我们用它来保存什么类型的对象,所以他们把集合设计成能保存任何类型的对象,只要求具有很好的通用性。但这样做带来如下两个问题:

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

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

        public class ListErr
        {
            public static void main(String[] args)
            {
                  // 创建一个只想保存字符串的List集合
                  List strList=new ArrayList();
                  strList.add("疯狂Java讲义");
                  strList.add("疯狂Ajax讲义");
                  strList.add("轻量级Java EE企业应用实战");
                  // “不小心”把一个Integer对象“丢进”了集合
                  strList.add(5);    //①
                  for (int i=0; i < strList.size() ; i++ )
                  {
                        // 因为List里取出的全部是Object,所以必须进行强制类型转换
                        // 最后一个元素将出现ClassCastException异常
                        String str=(String)strList.get(i);   //②
                  }
            }
        }

1.2 手动实现编译时检查类型

如果希望创建一个List对象,且该List对象中只能保存字符串类型,那么我们可以扩展ArrayList类。下面程序创建了一个StrList集合类,该集合里只能存放String对象。

        // 自定义一个StrList集合类,使用组合的方式来复用ArrayList类
        class StrList
        {
            private List strList=new ArrayList();
            // 定义StrList的add方法
            public boolean add(String ele)
            {
                  return strList.add(ele);
            }
            // 重写get()方法,将get()方法的返回值类型改为String类型
            public String get(int index)
            {
                  return (String)strList.get(index);
            }
            public int size()
            {
                  return strList.size();
            }
        }
        public class CheckType
        {
            public static void main(String[] args)
            {
                  // 创建一个只想保存字符串的List集合
                  StrList strList=new StrList();
                  strList.add("疯狂Java讲义");
                  strList.add("疯狂Android讲义");
                  strList.add("轻量级Java EE企业应用实战");
                  // 下面语句不能把Integer对象“丢进”集合中,否则将引起编译错误
                  strList.add(5);    //①
                  System.out.println(strList);
                  for (int i=0; i < strList.size() ; i++ )
                  {
                        // 因为StrList里元素的类型就是String类型
                        //所以无须进行强制类型转换
                        String str=strList.get(i);
                  }
            }
        }

这种做法虽然有效,但局限性非常明显——程序员需要定义大量的List子类,这是一件让人沮丧的事情。从Java 5以后,Java引入了“参数化类型(parameterized type)”的概念,允许我们在创建集合时指定集合元素的类型。Java的参数化类型被称为泛型(Generic)。

1.3 使用泛型

对于前面的ListErr.java程序,可以使用泛型改进这个程序。

        public class GenericList
        {
            public static void main(String[] args)
            {
                  // 创建一个只想保存字符串的List集合
                  List<String> strList=new ArrayList<String>();  //①
                  strList.add("疯狂Java讲义");
                  strList.add("疯狂Android讲义");
                  strList.add("轻量级Java EE企业应用实战");
                  // 下面代码将引起编译错误
                  strList.add(5);   //②
                  for (int i=0; i < strList.size() ; i++ )
                  {
                        // 下面代码无须进行强制类型转换
                        String str=strList.get(i);   //③
                  }
            }
        }

1.4 Java 7泛型的“菱形”语法

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

	List<String> strList=new ArrayList<>();
	Map<String , Integer> scores=new HashMap<>();
        public class DiamondTest
        {
            public static void main(String[] args)
            {
                  // Java自动推断出ArrayList的<>里应该是String
                  List<String> books=new ArrayList<>();
                  books.add("疯狂Java讲义");
                  books.add("疯狂Android讲义");
                  books.add("轻量级Java EE企业应用实战");
                  // 遍历时集合元素就是String
                  for (String book : books )
                  {
                        System.out.println(book);
                  }
                  //Java自动推断出HashMap的<>里应该是String, List<String>
                  Map<String , List<String>> schoolsInfo=new HashMap<>();
                  // Java自动推断出ArrayList的<>里应该是String
                  List<String> schools=new ArrayList<>();
                  schools.add("斜月三星洞");
                  schools.add("西天取经路");
                  schoolsInfo.put("孙悟空" , schools);
                  // 遍历Map时,Map的key是String类型
                  for (String key : schoolsInfo.keySet())
                  {
                        // value是List<String>类型
                        List<String> list=schoolsInfo.get(key);
                        System.out.println(key + "-->" + list);
                  }
                }
            }

2 深入泛型

所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)

2.1 定义泛型接口、类

下面是Java 5改写后List接口、Iterator接口、Map的代码片段。

        // 定义接口时指定了一个类型形参,该形参名为E
        public interface List<E>
        {
            // 在该接口里,E可作为类型使用
            // 下面方法可以使用E作为参数类型
            void add(E x);
            Iterator<E> iterator();   //①
            ...
        }
        // 定义接口时指定了一个类型形参,该形参名为E
        public interface Iterator<E>
        {
            // 在该接口里E完全可以作为类型使用
            E next();
            boolean hasNext();
            ...
        }
        // 定义该接口时指定了两个类型形参,其形参名为K、V
        public interface Map<K , V>
        {
            // 在该接口里K、V完全可以作为类型使用
            Set<K> keySet()   //②
            V put(K key, V value)
              .
        }

除此之外,我们发现①②处方法声明返回值类型是Iterator< E>、Set< K>,这表明Set< K>形式是一种特殊的数据类型,是一种与Set不同的数据类型——可以认为是Set类型的子类。

例如,我们使用List类型时,为E形参传入String类型实参,则产生了一个新的类型:List < String>类型,我们可以把List< String>想象成E被全部替换成String的特殊List子接口。

通过这种方式,就解决了1.2节中的问题——虽然程序只定义了一个List< E>接口,但实际使用时可以产生无数多个List接口,只要为E传入不同的类型实参,系统就会多出一个新的List接口。

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

通过上面介绍可以发现,我们可以为任何类、接口增加泛型声明(并不是只有集合类才可以使用泛型声明,虽然集合类是泛型的重要使用场所)。下面自定义一个Apple类,这个Apple类就可以包含一个泛型声明。

        // 定义Apple类时使用了泛型声明
        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());
            }
        }

注意:
当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明。例如,为Apple类定义构造器,其构造器名依然是Apple,而不是Apple!调用该构造器时却可以使用Apple的形式,当然应该为T形参传入实际的类型参数。Java 7提供了菱形语法,允许省略<>中的类型实参。

2.2 从泛型类派生子类

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

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

提示:
方法中的形参代表变量、常量、表达式等数据,本书把它们直接称为形参,或者称为数据形参。定义方法时可以声明数据形参,调用方法(使用方法)时必须为这些数据形参传入实际的数据;与此类似的是,定义类、接口、方法时可以声明类型形参,使用类、接口、方法时应为类型形参传入实际的类型

        // 使用Apple类时为T形参传入String类型
        public class A extends Apple<String>

如果从Apple类派生子类,则在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编译器可能发出警告:使用了未经检查或不安全的操作——这就是泛型检查的警告,读者在前一章中应该多次看到这样的警告。此时,系统会把Apple类里的T形参当成Object类型处理。

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

2.3 并不存在泛型类

前面提到可以把ArrayList类当成ArrayList的子类,事实上,ArrayList类也确实像一种特殊的ArrayList类,这个ArrayList对象只能添加String对象作为集合元素。但实际上,系统并没有为ArrayList生成新的class文件,而且也不会把ArrayList当成新类来处理。

看下面代码的打印结果是什么?

        // 分别创建List<String>对象和List<Integer>对象
        List<String> l1=new ArrayList<>();
        List<Integer> l2=new ArrayList<>();
        // 调用getClass()方法来比较l1和l2的类是否相等
        System.out.println(l1.getClass()==l2.getClass());

运行上面的代码片段,可能有读者认为应该输出false,但实际输出true。因为不管泛型的实际类型参数是什么,它们在运行时总有同样的类(class)。

不管为泛型的类型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。下面程序演示了这种错误。

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

泛型在对象创建时才知道是什么类型,但是静态方法属于类,调用test方法实际调用的Persion类的方法,而类在编译阶段就存在了,所以虚拟机根本不知道方法中引用的泛型是什么类型

        Collection cs=new ArrayList<String>();
        // 下面代码编译时引起错误:instanceof 运算符后不能使用泛型类
        if (cs instanceof List<String>) {...}

3 类型通配符(待更新)

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

考虑如下代码:

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

上面程序当然没有问题:这是一段最普通的遍历List集合的代码。问题是上面程序中List是一个有泛型声明的接口,此处使用List接口时没有传入实际类型参数,这将引起泛型警告。为此,我们考虑为List接口传入实际的类型参数——因为List集合里的元素类型是不确定的,将上面方法改为如下形式:

        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<>();
        // 将strList作为参数来调用前面的test方法
        test(strList);   //①

编译上面程序,将在①处发生如下编译错误:
上面程序出现了编译错误,这表明List对象不能被当成List对象使用,也就是说, List类并不是List类的子类。

**注意:**如果Foo是Bar的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口, G并不是G的子类型!

与数组进行对比

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

上面程序在①号粗体字代码处会引发ArrayStoreException运行时异常,这就是一种潜在的风险

提示:一门设计优秀的语言,不仅需要提供强大的功能,而且能提供强大的“错误提示”和“出错警告”,这样才能尽量避免开发者犯错。而Java允许Integer[]数组赋值给Number[]变量显然不是一种安全的设计。

在Java的早期设计中,允许Integer[]数组赋值给Number[]变量存在缺陷,因此Java在泛型设计时进行了改进,它不再允许把List对象赋值给List变量。

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

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

3.1 使用类型通配符

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

3.2 设定类型通配符的上限

3.3 设定类型形参的上限

4 泛型方法(待更新)

参考来源:《Java疯狂讲义》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值