Loading

Java泛型主题讨论

说明:在学习泛型这一知识点中,主要参考自《疯狂Java讲义》第7章P307-P330的泛型内容,因为是跳着阅读,所以前面的一些名词不是特别清楚,这里也做出适当备注,供自己识记与理解。

1.泛型

理解:说到泛型,感觉最初是为了解决Java集合的一个缺点——当我们想要把一个对象放进集合里面的时候,集合就会忘记这个对象的数据类型,再次把它取出来时,它的编译类型就会变成Object类型(运行类型不会变)。记住我们的目标是:在集合里面存储不会被忘记数据类型的各种对象。例子:

 1 package FanXing;
 2 
 3 public class ListErr {
 4      public static void main(String[] args) {
 5              List strList = new ArrayList();
 6             strList.add("泛型主题讨论");
 7             strList.add(017);//这里不小心把一个Integer对象放在了集合里面,可能报类型强制转换异常ClassCastException
 8            for(int i=0;i<strList.size();i++)
 9             {String str = (String)strList.get(i);
10                 }
11     }
12 }
编译信息:

Exception in thread "main" java.lang.Error: Unresolved compilation problems:
List cannot be resolved to a type
ArrayList cannot be resolved to a type

at FanXing.ListErr.main(ListErr.java:5)

图中的红色框已经提示我们需要用什么区解决所面临的的问题了。

为了达到我们的目标,我们想到了可以手动实现编译时去检查类型。

例子:(既然会发生异常那我们就在运行前先检查,我们这里先创建一个对象List,让它只保存字符串类型,这样就可以扩展ArrayList类)

package FanXing;

import java.util.ArrayList;
import java.util.List;

class StrList{
    private List strList = new ArrayList();
    public boolean add(String ele)//定义StrList的add方法,只添加字符串
    {
    return strList.add(ele);
    }
    public String get (int index)
    {
        return (String)strList.get(index);
    }
    public int size()
    {
        return strList.size();
    }
}

public class ListErr {
     public static void main(String[] args) {
             StrList strList = new StrList();
            strList.add("泛型主题讨论");
            //strList.add(017);如果没有这一句,代码可以成功被编译,否组会报错。
            System.out.println(strList);
           for(int i=0;i<strList.size();i++)
            {String str = strList.get(i);
                }
    }
}

上面的代码中我们定义的StrList类实现了编译时的异常检查,当编译到strList.add(017);时,程序试图将一个Integer对象加入到StrList集合中,程序在这里会无法编译通过,因为StrList只接受String的对象。

不过,既然只接受String对象的时候可以编译通过,说明这个方法还是有用的,但是,这种方法虽然有效,局限性却非常明显——我们需要去定义大量的List子类。

虽然这样也可以实现我们的目标:在集合里面存储不会被忘记数据类型的各种对象。不过这样非常非常麻烦,这个时候,我们的泛型就被设计出来了,有了它,我们的目标可以轻易实现。

package FanXing;

import java.util.ArrayList;
import java.util.List;

public class ListErr {
     public static void main(String[] args) {
             List<String> strList = new ArrayList<String>();//创建一个List集合,只保留字符串
             strList.add("泛型主题讨论");
             for(int i = 0;i<strList.size();i++)
             {
                 String str = strList.get(i);
             }
    }
}

很显然这样代码简化了很多,List<String>说明这是一个String类型的List。

所以这里我们可以归纳出,如果List<>尖括号里面是其他类型的话也是同理,即有了一个JDK1.5以后引入的概念:

Java泛型(generics)【Java的参数化类型】 :是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter)。声明的类型参数在使用时用具体的类型来替换。泛型最主要的应用是在JDK 5中的新集合类框架中。

泛型最大的好处是可以提高代码的复用性。以List接口为例,我们可以将String、Integer等类型放入List中,如不用泛型,存放String类型要写一个List接口,存放Integer要写另外一个List接口,泛型可以很好的解决这个问题。
2.深入泛型:

①定义泛型接口、类

public interface List<E>//定义接口,指定形参E,在这个接口里面E可以作为泛型使用
{
    void add(E,x);
    Iterator<E> iterator();//A
    ...
}
public interface Iterator<E>//在这个接口里,E可以作为类型使用
{
    E next();
    boolean hasNext();
    ...
}
public interface Map<K,V>//K,V可以作为类型使用
{
    Set<K> keySet()//B 
    V put (K key ,V value)
    ...
}

可以发现:在A、B处方法声明返回值类型是Iterrator<E>和Set<K>,说明他们是一种特殊的数据类型,可以认为是Iterrator和Set类型的子类。

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

public interface ListString extends List
{
    void add(String x);
    Iterator<String> iterator();
    ...
}

这样虽然只是设置了一个List<E>接口,实际实验时却是可以产生无数多个List 子接口。

【注意】包含泛型声明的类型可以在定义变量、创建对象时传入一个类型实参,从而可以动态的生成无数多个逻辑子表,但这种子类在物理上并不存在。也就是说,List<String>不会被替代成ListString,系统并没有进行源代码复制。

②从泛型类派生子类

当定义完泛型接口和泛型父类额时候,我们就可以为接口创建实现类或者从父类派生出子类,不过使用接口和父类的时候,不能再包含类型参数。

//错误演示

public class A extends List<E>{
     //A继承List,List不能跟类型形参
}   

//正确演示1
public class A extends List<String>{
     //A继承List,为List的E形参传入String
} 

//正确演示2
public class A extends List{
     //A继承List,也可以不为类型形参传入实际的类型参数,不过可能会出现unchecked警告
} 

③并不存在泛型类

前面有提到,可以把List<String>类当成是List的子类,这里可能会给大家带来误解,实际上,系统并没有为List<E>生成新的class文件,而且也不会把它当成新的类来处理。这里给一个验证:

package FanXing;

import java.util.ArrayList;
import java.util.List;

public class ListErr {
     public static void main(String[] args) {
             
             List<String> aaa = new ArrayList<>();
             List<Integer> aaa1 = new ArrayList<>();
             System.out.println(aaa1.getClass()==aaa1.getClass());
             
    }
}

从输出true可以看出,不管为泛型的类型形参传入哪一种类型实参,对于Java来说,他们依然被当做同一个类来处理,在内存中也只占用一块内存空间。

3.类型通配符

package FanXing;

import java.util.List;

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

 这是一个普通的遍历List集合的代码,在编译过程中出现了一个泛型警告,因为在这里使用List接口时没有传入实际的参数类型。

修改后:

package FanXing;

import java.util.List;

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

看到原来的List变成了List<?>,这里就引入了类型通配符的概念。

类型通配符就是一个“?”,它的元素类型可以匹配任何类型。

比如,当使用List<?>时,List就成了任何泛型List的父类,比如List既是List<String>的父类,又是List<Integer>的父类,但是,类型之间没有继承关系,String是Object的子类,List<String>不是List<Object>的子类。

①设置类型通配符的上限

格式:List<? extends XXX>它表示所有XXX泛型List的父类

②设定类型形参的上限

例子:

public class List<T extends Number & java.io.Serializable>
{
    ...//表明T类型必须是Number类或者其子类,并且必须实现java.io.Serializable接口
}

注意:为类型参数指定多个上限时,所有的接口上限必须位于类上限之后。

4.泛型方法

格式:

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

泛型方法和类型通配符的区别:

①大多数时候都可以用泛型方法来替换通配符
②使用通配符情况:用来支持灵活的子类化
③泛型方法允许类型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法。
④形参a的类型或返回值的类型依赖于另一个形参b的类型,则b的类型声明不应该使用通配符,因为使用通配符表示类型b不确定,那么a的类型也不能确定,这时候要考虑使用泛型方法。
⑤类型不被依赖时,使用通配符。

泛型方法与方法重载:

public class MyUtils {
  // (1)
  public static <T> void copy(Collection<T> dest, Collection<? extends T> src) {...}
  // (2)
  public static <T> T copy(Collection<? super T> dest, Collection<T> src) {...}
  public static void main(String[] args) {
    List<Number> ln = new ArrayList<>();
    List<Integer> li = new ArrayList<>();
    copy(ln, li); // 这里会编译报错
  }
}

允许根据方法参数泛型不同进行方法重载,但是调用时,如果编译器分不清该调用哪个方法则编译报错,上面代码中有两个copy方法,调用的时候,编译器既可以调用第一个copy ,也可以调用第二个copy,这样它就无法确定调用哪个,就会引起编译报错。

5.擦除和转换

 Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的List<Object>和List<String>等类型,在编译之后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方式与C++模板机制实现方式之间的重要区别。

参考链接:Java泛型使用详解

【备注1】:Java的编译类型和运行类型的理解。

Java的编译时类型是由声明变量时使用的类型决定,运行时类型是由实际赋值的对象所决定。参考链接:Java的编译类型和运行类型

【备注2】:Java泛型中K T V E ? 分别代表的含义:

E – Element (在集合中使用,因为集合中存放的是元素)

T – Type(Java 类)

K – Key(键)

V – Value(值)

N – Number(数值类型)

? – 表示不确定的java类型(无限制通配符类型)参考链接:泛型相关

【备注3】:为什么说在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型参数。

初步理解:

因为泛型是要在对象创建的时候才知道是什么类型的,而对象创建的代码执行先后顺序是static的部分,然后才是构造函数等等。所以在对象初始化之前static的部分已经执行了,如果你在静态部分引用的泛型,那么毫无疑问虚拟机根本不知道是什么东西,因为这个时候类还没有初始化。因此在静态方法、数据域或初始化语句中,为了类而引用泛型类型参数是非法的。

 实际原因:

静态变量是被泛型类所有实例所共享的。对于声明为MyClass<T>的类,访问其中的静态变量的方法仍然是MyClass.myStaticVar。不管是通过new MyClass<String>还是new MyClass<Integer>创建的对象,都是共享一个静态变量。假设允许类型参数作为静态变量的类型。那么考虑下面一种情况:

MyClass<String> class1 = new MyClass<String>();

MyClass<Integer> class2 = new MyClass<Integer>();

class1.myStaticVar = "hello";

class2.myStaticVar = 5;

由于泛型系统的类型擦除(type erasure)。myStaticVar被还原成Object类型,然后当调用class1.myStaticVar= "hello"; 编译器进行强制类型转换,即myStaticVar = (String)"hello";接着调用class2.myStaticVar语句时,编译器继续进行强制类型转换,myStaticVar = (Integer)Integer.valueOf(5); 此时myStaticVar是String类型的,当然该语句会在运行时抛出ClassCastException异常,这样一来存在类型安全问题。因此泛型系统不允许类的静态变量用类型参数作为变量类型。

当然,静态泛型方法也不允许。
参考链接:

有关静态不允许使用类型参数的讨论

为什么类型参数不能作为静态变量的类型

【备注4】:Java泛型中上下界限定符extends 和 super的理解:

<? extends T>表示类型的上界,表示参数化类型可能是T或者T的子类;

<? super T>表示类型的下界,表示参数化类型是此类型的超类型(父类型),直至Object。

PECS原则:

如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)

如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)

如果既要存又要取,那么就不要使用任何通配符。

【备注5】:异常类

异常类一般分为两种:异常(Exception)和错误(Error)

Exception就用try&catch&finally来处理,先在try中运行代码,catch处理可能出现异常,finally是一定会执行到里面的代码。 
常见的异常有: 
NumberFormatException(数字格式异常) 
IndexOutOfBoundsException(数组越界异常) 
ArithmeticException(除零异常) 
RuntimeException(运行时异常) 
异常常用的方法: 
getMessage():返回异常的详细描述字符串。 
printStackTrace():跟踪栈详细输出到标准错误输出 
printStackTrace(PrintStream s):跟踪栈详细输出到标准错误输出到指定的输出流 
getStackTrace():返回异常的跟踪栈信息。 

Error一般是由虚拟机造成的系统崩溃的。

下一步学习拓展及计划:

1.总结讨论并做成思维导图

2.理解学习中一直提到的异常这一章的内容

3.擦除的实例(自己尝试用一个例子实践)

posted @ 2019-03-27 19:44  Kingwan  阅读(312)  评论(0编辑  收藏  举报