JAVA-泛型

一、泛型是什么

一般的类和方法,只能使用具体的类型: 要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的 代码,这种刻板的限制对代码的束缚就会很大。----- 来源《Java编程思想》对泛型的介绍。
泛型是在JDK1.5引入的新的语法,通俗讲,泛型:就是适用于许多类型,但只限制于类类型,基本类型是不能做为参数的。从代码上讲,就是对类型实现了参数化。

二、泛型的使用场景

我们需要要以下代码来代入我们理解泛型:

实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值。
我们知道Object是所有类的父类,那我们就可以使用Object来实现。
public class MyArray {
    private Object[] array = new Object[10];

    public void set(int pos, Object val) {
        array[pos] = val;
    }

    public Object get(int pos) {
        return array[pos];
    }
}


class TestDemo {
    public static void main(String[] args) {
        MyArray myArray = new MyArray();
        myArray.set(0,1);//给0小标Integer类型
        myArray.set(1,2);//给1小标Integer类型
        myArray.set(2,"hallo");//给2小标String类型

        System.out.println(myArray.get(1));
        String s = myArray.get(2);//报错
        System.out.println(s);
    }
}

在获取2小标也就是String类型的时候报错了,强制类型转换为String类型后,不再报错。

结果:

 

但是我们发现有一些问题:

1. 任何类型数据都可以存放
2. 2号下标本身就是字符串,但是确编译报错。必须进行强制类型转换
也就是说什么东西都可以往里面放,各种类型乱七八糟的,而且我要用某个小标的值还需要提前知道里面是什么类型的值。
虽然在这种情况下,当前数组任何数据都可以存放,但是,更多情况下,我们还是希望他只能够持有一种数据类型。而不是同时持有这么多类型。所以,泛型的主要目的:就是指定当前的容器,要持有什么类型的对象。让编译器去做检查。此时,就需要把类型,作为参数传递。需要什么类型,就传入什么类型。

 三、泛型的基本使用

简单介绍一下,方便我们理解类型擦除机制,后面再详细介绍

class 泛型类名称<类型形参列表> {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> {
}
class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
}

那么我们将刚刚的MyArray给改成泛型类:

public class MyArray<T>{
    private Object[] array = new Object[10];//为什么Object不行为T

    public void set(int pos, T val) {
        array[pos] = val;
    }

    public T get(int pos) {
        return (T)array[pos];
    }
}

可能细心的朋友就要问:为什么数组类型不改为T仍然是Object

无论是哪一边改为T都不行,编译都编译不了:

最多可以这样,但是这样写并不算好,为什么一会再说:

最好还是写为:

private Object[] array = new Object[10];

 类名后的 <T> 代表占位符,表示当前类是一个泛型类,实际上就是个名字写成什么都可以,但是行业有自己的一些行业规范:

【规范】类型形参一般使用一个大写字母表示,常用的名称有:

E 表示 Element 数组元素
K 表示 Key
V 表示 Value
N 表示 Number
T 表示 Type
S, U, V 等等 - 第二、第三、第四个类型

泛型类的使用:

更推荐第二种方法

public static void main(String[] args) {
            
     //传入类型                           这里可以不传
     MyArray<Integer> myArray1 = new MyArray<Integer>();

     //推荐                       自动推导出类型
     MyArray<Integer> myArray = new MyArray<>();
        
}

 四、类型擦除机制

我们来到MyArray生成的字节码文件路径下,在文件路径输出cmd,再输入 javap -c 文件名 

可以看到所有关于T的类型全部被替换为了Object。

编译的过程当中,将所有的T替换为Object这种机制,我们称为:擦除机制

在 Java 中,泛型的擦除机制是在编译阶段发生的,编译后的字节码文件中泛型类型参数会被擦除,对于没有指定上界的泛型类型参数,会被替换为 Object 类型;

如果指定了上界(例如 class MyClass<T extends Number> ),则会被替换为所指定的上界类型(这里是 Number )

现在还不知道上界,不要紧后面看就懂了

可能到这里就有朋友有疑问了,反正泛型都会被替换为Object那为什么不能用泛型创建数组呢?

先讲直接用泛型创建的数组:

private T[] array = new T[10];
  • 我现在根本不知道它是什么类型,但是数组在创建的时候需要确定类型

用Object创建再强转的情况:

 private T[] array = (T[])new Object[10];

我们用一个代码来解释:

public class MyArray<T>{
    private Object[] array = new Object[10];

    public void set(int pos, T val) {
        array[pos] = val;
    }

    public T get(int pos) {
        return (T)array[pos];
    }

    public T[] getArray() {
        return (T[])array;
    }
}


class TestDemo {
    public static void main(String[] args) {
        MyArray<Integer> myArray = new MyArray<>();
        Integer[] ret = myArray.getArray();
    }
}

报了一个这样的异常:类型异常

Exception in thread "main" java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.Integer; ([Ljava.lang.Object; and [Ljava.lang.Integer; are in module java.base of loader 'bootstrap')
	at demo1.TestDemo.main(MyArray.java:23)
原因:替换后的方法为:将Object[]分配给Integer[]引用,程序报错
正确的方法:(了解)
class MyArray<T> {
    public T[] array;
    public MyArray() {
    }
    /**
     * 通过反射创建,指定类型的数组
     * @param clazz
     * @param capacity
     */
    public MyArray(Class<T> clazz, int capacity) {
        array = (T[]) Array.newInstance(clazz, capacity);
    }
    public T getPos(int pos) {
        return this.array[pos];
    }
    public void setVal(int pos,T val) {
        this.array[pos] = val;
    }
    public T[] getArray() {
        return array;
    }

    public static void main(String[] args) {
        MyArray<Integer> myArray1 = new MyArray<>(Integer.class,10);
        Integer[] integers = myArray1.getArray();
    }
}

五、泛型类

1.泛型类的定义

class 泛型类名称<类型形参列表> {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> {
}
class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
}
public class Test<T> {
    public T key;

    public void set(T key) {
        this.key = key;
    }

    public T get() {
        return this.key;
    }
}

在泛型类中,类型参数定义的位置有三处,分别为:

  • 非静态的成员变量类型
  • 非静态的常用方法形参
  • 非静态的成员方法返回类型

其中静态的成员方法,可以用泛型方法的方式使用泛型,后面泛型方法讲解

2.泛型类的使用

public static void main(String[] args) {
        //推荐
        //在<>内传入你想使用的类型,后面的<>内可以不写会自动推导
        Test<Integer> test = new Test<>();
        
        //显示定义
        Test<Integer> test1 = new Test<Integer>();
}
  • 当我们传入Integer类型之后就不能传入其他类型了,如果传入其他类型编译器会直接报错,所以使用泛型是有安全检查机制,能帮我们更好的管理数据。
  • 值得注意的是,泛型类,类型的确定是在实例化对象的时候,在此之前是Object

3.泛型被替换后

public class Test<Integer>{
    public Integer key;

    public void set(Integer key) {
        this.key = key;
    }

    public Integer get() {
        return this.key;
    }
} 

4.裸类型(Raw Type) (了解)

如果我们在实例化泛型类时,没有指定类型:

Test test = new Test();

那么里面的泛型类都会变为Object 

public class Test {
    public Object key;

    public void set(Object key) {
        this.key = key;
    }

    public Object get() {
        return this.key;
    }
} 

注意: 我们不要自己去使用裸类型,裸类型是为了兼容老版本的 API 保留的机制
下面的类型擦除部分,我们也会讲到编译器是如何使用裸类型的。
小结:
1. 泛型是将数据类型参数化,进行传递
2. 使用 <T> 表示当前类是一个泛型类。
3. 泛型目前为止的优点:数据类型参数化,编译时自动进行类型检查和转换

六、泛型接口 

1.泛型接口的定义

泛型接口和泛型类差别不大:

public interface 接口名<类型形参列表> {
    ...
}
public interface IUsb<T> {
//    public T key;  报错,接口中的成员是静态的: public static final

    public T get(T key); //抽象方法可以使用: public abstract

    //默认的方法也可以使用
    default T Func(T key) {
        return key;
    }
}

2.泛型接口的使用

泛型类实现泛型接口

class AA<T> implements IUsb<T> {

    @Override
    public T get(T key) {
        return null;
    }
}

普通类实现泛型接口:

就需要指定类型了

class BB implements IUsb<Integer> {

    @Override
    public Integer get(Integer key) {
        return null;
    }
}

接口情况:

泛型接口继承泛型接口:

interface AC<T> extends IUsb<T> {
    //……
}
class CC implements AC<String> {

    @Override
    public String get(String key) {
        return null;
    }
}

都是泛型类:

interface AC<T> extends IUsb<T> {
    //……
}
class CC<T> implements AC<T> {

    @Override
    public T get(T key) {
        return null;
    }
}

总结:

  • 如果是泛型类实现或继承某个接口或类,在实例化对象的时候才确定具体类型。
  • 如果不是泛型类或接口实现或继承某个接口或类,就需要在定义接口或类的时候指定类型
  • 且接口中的成员是静态的,不能使用泛型

 

七、泛型方法

1.泛型方法的定义

要区分好使用泛型类泛型的方法,和本身就是泛型方法的区别:

public <类型参数> 返回类型 方法名(类型参数 变量名) {
    ...
}

只有在方法返回值类型前面写了<?>的时候才是泛型方法

public class Test<T> {
    public T key;

    public void set(T key) {
        this.key = key;
    }

    public T get() {
        return this.key;
    }
    //这里的泛型虽然和类同名,但是在此方法里面,用的是方法的泛型
    public <T> T test(T t) {
        return t;
    }
}

 2.泛型方法的使用

public static void main(String[] args) {
        Test<Integer> test = new Test<>();
        //传入一个不一样的类型参数,证明它们不一样
        System.out.println(test.test("hallo"));

        //或者也可以显示定义类型,但一般没有这么写。不写也会根据你给的值确定类型
        System.out.println(test.<String>test("bit"));
}

结果:

3.静态泛型方法的定义

 //这里的泛型虽然和类同名,但是在此方法里面,用的是方法的泛型
    public static <T> T test(T t) {
        return t;
    }

 

4.静态泛型方法的使用

public static void main(String[] args) {
        //通过类名直接调用
        Test.test("hello");

        //或者
        Test.<String>test("word");
}

5.为什么静态方法可以使用泛型

不知道大家有没有这样的问题,为什么泛型类里面的静态方法不能使用泛型,但是静态方法却可以?

我们知道,泛型类里面的静态方法不能使用是因为,泛型类的类型参数是在实例化对象时才确定的,而静态方法在类加载时就存在,此时类的泛型类型参数还未确定,

静态泛型方法有自己独立声明的泛型类型参数,这个类型参数是在方法调用时才确定具体类型的,与泛型类的实例化无关。

 

八、泛型的上界

class 泛型类名称<类型形参 extends 类型边界> {
...
}
public class MyArray<E extends Number> {
    ……
}

意思是:你传入的参数类型(E)必须是Number 的子类或者它自己 

 更复杂一点的:

public class MyArray<E extends Comparable<E>> {
...
}

 你传入的参数类型 E 必须是实现了Comparable,且类型是E

泛型的下届不能直接像泛型的上界一样使用,代码会报错:

但是通配符是可以使用下届的

九、通配符

 ? 用于在泛型的使用,即为通配符

1.通配符的作用 

有这样一段程序

class Message<T> {
    private T message ;
    public T getMessage() {
        return message;
    }
    public void setMessage(T message) {
        this.message = message;
    }
}
class TestDemo {
    public static void main(String[] args) {
        Message<String> message = new Message<>() ;
        message.setMessage("比特就业课欢迎您");
        fun(message);
    }
    //                   如果我们传入的不是Stirng类型
    public static void fun(Message<String> temp){
        System.out.println(temp.getMessage());
    }
}

如果我们不想只传入String类型,还想传入各种类型,并且我不希望里面的值被更改

此时就可以使用通配符了:

class Message<T> {
    private T message ;
    public T getMessage() {
        return message;
    }
    public void setMessage(T message) {
        this.message = message;
    }
}
class TestDemo {
    public static void main(String[] args) {
        Message<String> message = new Message<>() ;
        message.setMessage("比特就业课欢迎您");
        fun(message);
    }
    public static void fun(Message<?> temp){
        temp.setMessage("hello");//无法修改
        System.out.println(temp.getMessage());
    }
}

 那我们想传入各种类型,但是允许更改数据我们继续用普通的泛型就可以了

public static <T> void fun(Message<T> temp){
        temp.setMessage((T)"hello");//此时就可以做到更改了
        System.out.println(temp.getMessage());
}

那我们希望可以指定一下传入数据的范围,就可以使用通配符的上界和下届:

? extends 类:设置通配符上限
? super 类:设置通配符下限

2.通配符的上界

语法:

<? extends 上界>
<? extends Number>//可以传入的实参类型是Number或者Number的子类
class Animal{

}

class Dog extends Animal{

}

class Cat extends Animal{

}

class Message<T> { // 设置泛型
    private T message ;
    public T getMessage() {
        return message;
    }
    public void setMessage(T message) {
        this.message = message;
    }
}

public class Test {
    public static void main(String[] args) {
        Message<Dog> message = new Message<>();
        message.setMessage(new Dog());
        fun(message);

    }

    // 此时?可以接收Animal的子类,但是由于不确定类型,所以无法修改
    public static void fun(Message<? extends Animal> temp){
//        temp.setMessage(new Dog()); //仍然无法修改!
//        temp.setMessage(new Animal()); //仍然无法修改!
//        temp.setMessage(new Cat()); //仍然无法修改!
        Animal animal = temp.getMessage();//但是可以接收
        System.out.println(animal);
    }
}

因为我们无法确定到底传入的是哪一个子类,所有通配符的上界是无法对内容进行更改的。

因此:通配符的上界,不能进行写入数据,只能进行读取数据

那么普通的泛型上界,可以写入读取数据吗:

    public static void main(String[] args) {
        Message<Dog> message = new Message<>();
        message.setMessage(new Dog());
        fun(message);
    }

    public static <T extends Animal> void fun(Message<T> tmp) {
        tmp.setMessage((T) new Dog());
        tmp.setMessage((T) new Cat());
        Animal animal = tmp.getMessage();
        System.out.println(animal);
    }

最终输出了Cat对象的地址,答案是可以做到的。

3.通配符的下界

语法

<? super 下界>
<? super Integer>//代表 可以传入的实参的类型是Integer或者Integer的父类类型

        

class Animal{

}

class Dog extends Animal{

}

class DDD extends Dog {
    
}
class Cat extends Animal{

}

class Message<T> { // 设置泛型
    private T message ;
    public T getMessage() {
        return message;
    }
    public void setMessage(T message) {
        this.message = message;
    }
}

public class Test {
    public static void main(String[] args) {
        Message<Dog> message = new Message<>();
        message.setMessage(new Dog());
        fun(message);
    }



    public static void fun(Message<? super Dog> temp){
    // 此时可以修改!!添加的是Dog 或者 Dog的子类
        temp.setMessage(new DDD());//这个是Dog的子类
        temp.setMessage(new Dog());//这个是Dog的本身
//        Dog dog = temp.getMessage(); 不能接收,这里无法确定是哪个父类
        System.out.println(temp.getMessage());//只能直接输出
    }
}
通配符的下界,不能进行获取数据,只能写入数据
这里可能有点绕我画个图解释一下:

 

### Java练习题及解析 #### 1. 类的基本使用 考虑以下类的定义: ```java class Holder<T> { T value; public Holder(T value) { this.value = value; } public T getValue() { return value; } } ``` 该类定义了一个类 `Holder`,其中 `T` 是一个类参数。可以使用该类来存储任何类的对象,并通过 `getValue()` 方法获取该对象。例如,以下代码演示了如何创建一个 `Holder` 实例来存储一个字符串: ```java Holder<String> stringHolder = new Holder<>("Hello"); System.out.println(stringHolder.getValue()); // 输出 "Hello" ``` 如果尝试将错误类的对象传递给 `Holder`,编译器会报错。例如,以下代码会导致编译错误: ```java Holder<Integer> intHolder = new Holder<>("Hello"); // 编译错误 ``` 这是因为 `Holder<Integer>` 要求传入的值必须是 `Integer` 类,而 `"Hello"` 是 `String` 类。 #### 2. 原始类的兼容性 Java 允许将类的实例赋值给原始类的变量,但这种做法不推荐,因为它会失去类安全性。例如: ```java Holder rawHolder = new Holder<String>("Hello"); String value = rawHolder.getValue(); // 不推荐,但可以编译通过 ``` 尽管这段代码可以编译通过,但它失去了带来的类检查。如果尝试从 `rawHolder` 获取一个非 `String` 类的对象,可能会在运行时抛出 `ClassCastException`。 #### 3. 类推断的增强 在 Java 23 中,类推断得到了增强,允许在某些情况下省略显式的类参数。例如: ```java List<String> list = Collections.emptyList(); // Java 23 支持空目标类推断 ``` 在旧版本的 Java 中,必须显式指定类参数: ```java List<String> list = Collections.<String>emptyList(); ``` 这种改进使得代码更加简洁,并减少了冗余的类声明。 #### 4. 可变参数与的结合 Java 允许将可变参数与结合使用。例如,可以定义一个方法来接受可变数量的参数: ```java public static <T> void printValues(T... values) { for (T value : values) { System.out.println(value); } } ``` 调用该方法时,可以传入任意数量的参数: ```java printValues("Apple", "Banana", "Cherry"); // 输出三个字符串 printValues(1, 2, 3); // 输出三个整数 ``` 这种方法在处理不确定数量的输入时非常有用,并且保持了类安全性。 #### 5. 方法的使用 不仅可以用于类,还可以用于方法。例如,定义一个方法来交换两个元素的位置: ```java public static <T> void swap(T[] array, int i, int j) { T temp = array[i]; array[i] = array[j]; array[j] = temp; } ``` 调用该方法时,可以传入任意类的数组和索引: ```java String[] names = {"Alice", "Bob"}; swap(names, 0, 1); System.out.println(Arrays.toString(names)); // 输出 "[Bob, Alice]" ``` 这种方法可以应用于任何类的数组,并且保证了类安全性。 #### 6. 接口的实现 Java 允许定义接口,并由具体的类实现这些接口。例如,定义一个接口 `Container<T>`: ```java interface Container<T> { void add(T item); T get(int index); } ``` 然后,可以实现该接口的具体类: ```java class StringContainer implements Container<String> { private List<String> items = new ArrayList<>(); @Override public void add(String item) { items.add(item); } @Override public String get(int index) { return items.get(index); } } ``` 这样,`StringContainer` 类只能用于处理 `String` 类的对象,确保了类安全性。 #### 7. 的边界限制 Java 支持通过边界限制来约束类参数的范围。例如,定义一个方法,要求类参数必须是 `Number` 的子类: ```java public static <T extends Number> double sum(T[] numbers) { double total = 0; for (T number : numbers) { total += number.doubleValue(); } return total; } ``` 调用该方法时,只能传入 `Number` 的子类数组: ```java Integer[] integers = {1, 2, 3}; Double[] doubles = {1.5, 2.5, 3.5}; System.out.println(sum(integers)); // 输出 6.0 System.out.println(sum(doubles)); // 输出 7.5 ``` 这种方法确保了类的安全性和操作的正确性。 #### 8. 多类参数的使用 Java 支持多个类参数的定义。例如,定义一个类 `Pair<K, V>`,用于存储键值对: ```java class Pair<K, V> { K key; V value; public Pair(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; } } ``` 调用该类时,可以传入任意类的键和值: ```java Pair<String, Integer> pair = new Pair<>("Age", 25); System.out.println(pair.getKey()); // 输出 "Age" System.out.println(pair.getValue()); // 输出 25 ``` 这种方法可以灵活地处理不同类的数据组合。 #### 9. 的通配符使用 Java 支持使用通配符 `?` 来表示未知类。例如,定义一个方法来打印任意类的列表: ```java public static void printList(List<?> list) { for (Object obj : list) { System.out.println(obj); } } ``` 调用该方法时,可以传入任意类的 `List`: ```java List<String> stringList = Arrays.asList("A", "B", "C"); List<Integer> integerList = Arrays.asList(1, 2, 3); printList(stringList); // 输出三个字符串 printList(integerList); // 输出三个整数 ``` 这种方法在处理未知类的数据时非常有用,并且保持了类安全性。 #### 10. 的递归类限制 Java 支持递归类限制,允许类参数继承自身。例如,定义一个接口 `SelfBound<T>`: ```java interface SelfBound<T extends SelfBound<T>> { T self(); } ``` 这种设计模式常用于链式调用和构建器模式中,确保返回的类与当前类一致。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值