JAVA泛型详解

一:为什么引入泛型机制:

假如我们想要实现一个String数组,并且要求它可以动态改变大小,这时会想到使用ArrayList来聚合String对象。然而,过一阵子,我们想要实现一个大小可以改变的Date对象数组,这时我们希望可以重用之前写过的那个针对String对象的ArrayList实现。

//在JAVA 5之前,ArrayList的实现大致如下:
public class ArrayList{
        public Object get(int i){...}
        public void add(Object obj){...}
        ...
        private Object[] elementData;
}

从上面代码可以看到,用于向ArrayList中添加元素的add方法接受一个Object型参数,从ArrayList获取指定元素的get方法也返回一个Object类型的对象,Object对象数组elementData存放这个ArrayList中的对象,也就是说无论你向ArrayList中放入什么类型的对象,到了它的内部,都是一个Object对象。
所以基于继承的泛型实现会带来两个问题:
1. GET方法:每次调用get方法都会返回一个Object对象,每次都需要强制类型转换为我们需要的类,麻烦。
2. ADD方法:加入向我们的String对象的ArrayList中加入一个File对象,编译器不会报错,而这不是我们想要的。
所以从JAVA 5开始,ArrayList在使用时可以加上一个类型参数,这个类型参数用来指明ArrayList中的元素类型,类型参数的引入解决了以上的两个问题。代码如下:

ArrayList<String> s = new ArrayList<String>();
s.add("abc");
String s = s.get(0);//无需强制转换
s.add(123);//编译错误,只能向其中添加String对象

编译器获知ArrayList的类型参数String后,便会替我们完成强制类型转换以及类型检查工作

二:泛型类:

先定义一个简单的Box类:

public class Box {
    private String object;
    public void set (String object){this.object = objec}
    public String get(){return object;}
}

这是最常见的做法,这样做的一个坏处是Box里面现在只能装入String类型的元素,今后如果还要装入Integer等其他类型的元素,还必须要另外重写一个Box,代码得不到复用,使用泛型可以解决。

public class Box<T>{
    private T t;
    public void set(T t){this.t = t;}
    public T get(){return t;}
}

这样Box类就可以复用,可以将T替换成任何我们想要的类型

Box<Integer> integerBox = new Box<Integer>();
Box<Double> integerBox = new Box<Double>();
Box<String> integerBox = new Box<String>();

三:泛型方法:

所谓泛型方法,就是带有类型参数的方法,它既可以定义在泛型类中也可以定义在普通类中。

public class ArrayAlg {
    public static <T> T getMiddle(T[] a){
        return a[a.length/2];
    }
}

以上代码中的getMiddle方法既为一个泛型方法,定义的格式是类型变量放在修饰符的后面,返回类型的前面。可以看到,以上泛型方法可以针对各种类型的数组调用,在这些数组的类型已知有限时,也可以用重载实现,不过编码效率很低。调用以上方法如下:

String[] strings = {"aa","bb","cc"};String middle = ArrayAlg.getMiddle(strings);

四:边界符:

现在我们要实现这样一个功能:查找一个泛型数组中大于某个特定元素的个数,可以这样实现:

public static <T> int countGreaterThan(T[] anArray,T elem){
    int count = 0;
    for(T e : anArray){
        if(e > elem)
            ++count;
    }
    return count;
}

有一个明显的错误,因为除了short,int,double,long,float.byte,char等原始类型参数,其他的类并不一定能使用操作符”>“,所以编译器报错,解决方法——使用边界符。

Public interface Comparable<T>{
    public int compareTo(T o);
}

做一个类似下面这种声明,等于告诉编译器类型参数T代表的都是实现了Comparable接口的类,这样等于告诉编译器它们都至少实现了comparaTo方法。

public static <T extends Comparable<T>> int countGreaterThan(T[] anArray,T elem){
    int count = 0;
    for(T e : anArray){
        if(e > elem)
            ++count;
    }
    return count;
}

五:通配符:

在某些情况下,泛型类或者泛型方法想要对自己的类型参数进一步加一些限制。比如,想要限定类型参数只能为某个类的子类或者只能为实现某个接口的类。相关语法——(BoundingType为一个类或者一个接口,可以多于一个,用”&“链接即可)
为了更清楚了解通配符,举个例子还是借用上面的Box类,假设添加一个这样的的方法:

Public void boxTest(Box<Number> n ){...}

那么Box n 允许接受什么类型的参数呢?我们是否能够传入Box或者Box呢?答案是否定的。虽然Integer和Double是Number的子类,但是在泛型中Box或者Box与Box之间没有任何关系!!!
定义几个简单的类,下面将用到:

class Fruit {}
class Apple extends Fruit {}
class Orange extends Fruit {}

下面这个例子中,我们创建了一个泛型类Reader,然后在f1()中当我们尝试Fruit f = fruitReader.readExact(apples);编译器会报错,因为List和List之间没有任何关系。

public class GenericReading {
    static List<Apple> apples = Arrays.asList(new Apple());
    static List<Fruit> fruit= Arrays.asList(new Fruit());
    static class Reader<T> {
        T readExact(List<T> list){
            return list.get(0);
        }
    }
    static void f1(){
    Reader<Fruit> fruitReader = new Reader<Fruit>();
    //ERRORS:List<Fruit> cannot be applet to List<Apple>
    //Fruit f = fruitReader.readExact(apples);
    }
    public static void main (...){
        f1();
    }
}

但是按照通常思维,Apple和Fruit之间肯定是存在联系的,然而编译器却无法识别,那怎么在泛型代码中解决?——通配符!!!

static class CovariantReader<T> {
    T readCovariant(List<? extends T> list){
        return list.get(0);
    }
}
static void f2(){
    CovariantReader<Fruit> fruitReader = new CovariantReader<Fruit>();
       Fruit f = fruitReader.readCovariant(fruit);
       Fruit a = fruitReader.readCovariant(apples);
}
public static void main(String[] args){
    f2();
}

这样就相当于告诉编译器,fruitReader的readCovariant方法接受的参数只要满足Fruit的子类就行(包括Fruit自身),这样子类和父类直接的关系也就关联上了。

六:PECS原则:

上面我们看到了类似

public class GenericsAndCovariance {
    public static void main(String[] args){
        List<? extends Fruit> flist = new ArrayList<Apple>();
        //Compile ERRORS:cannot add any type of object:
        //flist.add(new Apple());
        //flist.add(new Orange());
        //flist.add(new Fruit());
        //flist.add(null);//Legal but uninteresting
        Fruit f = flist.get(0);
    }
}

答案否定,JAVA编译器不允许这样做,为什么呢?从编译器的角度去考虑,因为<? extends Fruit> flist它自身可以有多种含义:

List<? extends Fruit> flist = new ArrayList<Fruit>();
List<? extends Fruit> flist = new ArrayList<Apple>();
List<? extends Fruit> flist = new ArrayList<Orange>();
  • 当我们尝试add一个Apple的时候,filst可能指向new ArrayList();
  • 当我们尝试add一个Orange的时候,flist可能指向new ArrayList();
  • 当我们尝试add一个Fruit的时候,这个Fruit可以是任何类型的Fruit,而flist可能指向某种特定类型的Fruit,编译器无法识别所以会报错。

所以对于实现了

public class GenericWriting {
    static List<Apple> apple = new ArrayList<Apple>();
    static List<Fruit> fruit = new ArrayList<Fruit>();
    static <T> void writeExact(List<T> list,T item){
        list.add(item);
    }
    static void f1(){
        writeExact(apples,new Apple());
        writeExact(fruit,new Apple());
    }
    static <T> void writeWithWildcard(List<? super T>,T item){
        list.add(item);
    }
    static void f2(){
        writeWithWildcard(apples,new Apple());
        writeWithWildcard(fruit,new Apple());
    }
    public static void main(String[] args) {
        f1();
        f2();
    }
}    

这样我们就可以往容器里面添加元素了,但是使用super的坏处是以后不能get容器里面的元素,原因很简单,还是从容器角度去考虑,对于List

List<? super Apple> list = new ArrayList<Apple>();
List<? super Apple> list = new ArrayList<Fruit>();
List<? super Apple> list = new ArrayList<Orange>();

当尝试通过list来get一个Apple时,可能会get到一个Fruit,这个Fruit可以是Orange等其他类型的Fruit。
根据上面例子,可以总结一个规律:“Producer Extends,Consumer Super”

  • Producer Extends”—如果你需要一个只读LIst,用它来Producer T,那么使用? extends T。
  • “Consumer Super” —如果你需要一个只写List,用它来consumer T,那么使用?super T。
  • 如果需要同时读取以及写入,那么就不能使用通配符了。

阅读一些JAVA集合类的源码,可以发现通常将两者结合使用,如下:

public class Collections {
    public static <T> void copy(List<? super T> dest,List<? extends T>src) {
        for (int i=0;i<src.size();i++)
            dest.set(i,src.get(i));
    }
}

七:类型擦除:

类型擦除就是说JAVA反省只能用于在编译期间的静态类型检查,然后编译器生成的代码会擦除相应的类型信息,这样到了运行期间实际上JVM根本就不知道泛型所代表的具体类型。
目的:因为JAVA泛型1.5之后才被引入的,为了保持向下的兼容性,所以只能做类型擦除来兼容以前的非泛型代码。

<think>嗯,用户想了解Java详解。首先,我需要回忆一下Java的基本概念和应用场景。Java 5引入的特性,主要用于增强类安全,减少类转换的麻烦,以及提高代码的可重用性。用户可能是在学习过程中遇到了相关的困惑,或者在实际开发中需要更深入的理解。 接下来,用户的需求可能包括了解的基本语法、类擦除、通配符、方法、类与接口,以及使用限制等。需要分步骤讲解,确保结构清晰。可能需要从为什么需要开始,然后逐步展开各个部分。 要注意的是,用户可能对类擦除这一Java的实现机制不太清楚,需要详细解释。同时,通配符部分,尤其是上下界通配符的区别和使用场景,可能是容易混淆的地方,需要举例说明。此外,在集合框架中的应用是一个常见的例子,可以用来帮助用户理解。 还需要考虑用户可能的误区,比如在运行时类信息被擦除的问题,导致无法进行instanceof判断或者创建数组。这些限制需要明确说明,避免用户在实际使用中犯错。 另外,用户可能对方法与类的区别不太清楚,需要分别举例说明。比如,方法可以在普通类中定义,而类则是整个类作用于类参数。 最后,要确保语言通俗易懂,避免过于技术性的术语,同时结合代码示例,让用户能够通过实例理解抽象的概念。检查是否有遗漏的重要知识点,比如继承、通配符捕获等,但可能根据用户的提问深度决定是否展开。</think>以下是对Java的系统化详解,分步骤解析核心概念和用法: --- ### 一、解决的问题 1. **类安全** 在集合等场景中,编译时检测类错误,避免运行时`ClassCastException` ```java // 非写法(存在隐患) List list = new ArrayList(); list.add("abc"); Integer num = (Integer) list.get(0); // 运行时异常 // 写法(编译时报错) List<String> list = new ArrayList<>(); list.add(123); // 编译错误 ``` 2. **消除强制类转换** 明确容器存储类,减少冗余代码 --- ### 二、基础语法 #### 1. 类 ```java public class Box<T> { private T content; public void setContent(T content) { this.content = content; } public T getContent() { return content; } } ``` - `T`为类参数(可任意命名,常用`T`, `E`, `K`, `V`等) - 实例化时指定具体类: ```java Box<Integer> intBox = new Box<>(); ``` #### 2. 接口 ```java public interface Comparator<T> { int compare(T o1, T o2); } ``` #### 3. 方法 ```java public <T> void printArray(T[] array) { for (T element : array) { System.out.println(element); } } ``` - 类参数声明在方法返回值前 --- ### 三、类擦除(关键机制) 1. **编译时类检查** 编译器检查合法性 2. **运行时类擦除** 生成的字节码中不包含信息,替换为原始类 ```java List<String> list = new ArrayList<>(); // 编译后等效于 List list = new ArrayList(); ``` 3. **桥方法生成** 用于保持多态性(如继承类时) --- ### 四、通配符 #### 1. 上界通配符 `<? extends T>` ```java void process(List<? extends Number> list) { // 允许读取为Number类 Number num = list.get(0); // 不允许添加元素(编译器无法确定具体子类) } ``` #### 2. 下界通配符 `<? super T>` ```java void addNumbers(List<? super Integer> list) { list.add(42); // 允许添加Integer及其父类元素 Object obj = list.get(0); // 读取只能作为Object } ``` #### 3. 无限定通配符 `<?>` ```java void printList(List<?> list) { for (Object elem : list) { System.out.println(elem); } } ``` --- ### 五、重要限制 1. **无法实例化类参数** ```java T obj = new T(); // 编译错误 ``` 2. **不能创建数组** ```java T[] array = new T[10]; // 编译错误 ``` 3. **静态上下文中不可用** ```java public class Test<T> { private static T instance; // 错误 } ``` 4. **instanceof检测无效** ```java if (list instanceof ArrayList<String>) { ... } // 编译错误 ``` --- ### 六、高级技巧 1. **类参数多重限定** ```java <T extends Comparable & Serializable> ``` 2. **与反射** ```java Type type = new TypeToken<Map<String, Integer>>(){}.getType(); ``` 3. **PECS原则** Producer-Extends, Consumer-Super (适合选择通配符边界的场景) --- ### 七、典应用场景 1. 集合框架(`ArrayList<E>`, `HashMap<K,V>`) 2. 工具类方法(`Collections.sort()`) 3. 回调接口设计(`Comparator<T>`) 4. 自定义通用数据结构 --- 通过掌握这些核心概念,可以写出类更安全、复用性更强的Java代码。建议结合具体项目实践加深理解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值