Java--泛型

本文详细阐述了为何使用泛型,其带来的编译期类型安全优势,以及泛型类、接口和方法的用法。讲解了泛型的约束、局限性及类型擦除,涉及C#和Java的区别。重点介绍了通配符类型的应用,以及在不同场景下的继承规则。

泛型:参数化类型

为什么要使用泛型

因为泛型提供了编译期的类型安全,确保只能把正确类型的对象放入集合中,避免在运行时出现ClassCastException。

使用泛型的好处:

1、可以让多种数据类型执行相同的代码;

2、泛型在使用的时候指定类型,不需要强制类型转换。

泛型类

将类型由原来的具体类型参数化:

public class MyClass<T> {
  private T data;
  public MyClass(){};
}

public class MyClass<T,K> {
  private T data1;
  private K data2;
}

泛型接口

public interface MyInter<T> {
  public void doSth();
}

实现泛型接口有两种实现方式:

未传入泛型实参:

public class MyClass<T> implements MyInterFace<T> {
    private T data;
    @Override
    public T doSth() {
        return null;
    }
}

//这样在new该类的时候需要指定具体的类型,不然程序无法识别我们需要的类型,默认是Object
public static void main(String[] args) {
        MyClass<Person> personMyClass = new MyClass<>();
        personMyClass.doSth();
    }

传入了泛型实参:

public class MyClass implements MyInterFace<Person> {
    private Person data;
    @Override
    public Person doSth() {
        return null;
    }
}

//这样就是我们平时正常的new一个对象调用构造方法
public static void main(String[] args) {
    MyClass personMyClass = new MyClass();
    personMyClass.doSth();
}

泛型方法

泛型方法的格式是:  权限修订符  <E> 返回值类型  方法名(){}

并不是说写在泛型类中的方法就叫泛型方法,泛型方法要遵守上面的那个格式,而且方法的类型参数可以和类的类型参数不同。

public class MyClass<T>{
    //普通方法
    public void doRunning(T obj) {
    }
    //普通方法
    public T doSpeaking(){
        return null;
    }
    //泛型方法,方法的类型E可以和类的类型T不同
    public <E> void doWorking (T obj){
    }
}

限定类型变量

public static <T extends Comparable> T min(T a, T b) {
    return a.compareTo(b) > 0 ? a : b;
}

这里 extends 实为派生,不单单指继承的范畴,也包括实现,这里实现该接口就是为了保证参数a 和 b都有compareTo方法可以使用,同样可以这样用:

public static <T extends Person & Comparable> T min(T a, T b) {
    a.doPersonFun();
    return a.compareTo(b) > 0 ? a : b;
}

以上代码中,T类型派生自Person 类和Comparable接口,这里的泛型类型T可以同时继承类或者实现接口,但是继承和实现同时存在的时候,继承的类要放在第一个,而且只能继承一个父类,像我们平时写代码自定义类的时候也是一样的道理:

public class MyClass<T> extends Person implements MyInterFace<T>,Comparable{
    @Override
    public T doSth() {
        return null;
    }
    @Override
    public int compareTo(Object o) {
        return 0;
    }
}

而且extends的左右都允许有多个比如:

public static <T,E extends Person & Comparable & Serializable> void min(T a, E b, E c) {
    a.hashCode();
    Object m = b.compareTo(c) > 0 ? b : c;
}

public static <T extends Comparable,E extends Person & Comparable & Serializable> void min(T a, E b) {
    Object m = a.compareTo(b) > 0 ? a : b;
}

extends前面的多个,使用逗号隔开,每个逗号隔开的泛型都是独立的,如第一个方法中 a (类型T)就只有普通类型的方法可用,b(类型E),就可以使用Compareble和Serializable中的接口方法。

泛型的约束性和局限性

不能用基本类型实例化类型参数:使用泛型在传入类型时,需要是封装类型,基本数据类型不可用;

MyClass myClass = new MyClass<Integer>(); // 如果传int 会报错

运行时类型查询只适用于原始类型:不能使用instanceof来判断是否是某个泛型类

// 这里的MyClass<Integer> 和 MyClass<T> 会报错
if (myClass instanceof MyClass<Integer>) { }

if (myClass instanceof MyClass<T>) { }
//只能是 myClass1 instanceof MyClass

变量myClass的类型只和原生类MyClass有关,和传入的泛型的类型无关

MyClass myClass1 = new MyClass<Integer>();
MyClass myClass2 = new MyClass<String>();
System.out.println(myClass1.getClass() == myClass2.getClass()); // true

泛型类的静态上下文中类型变量失效:静态变量和静态方法中不能用泛型做参数或者方法返回值类型:

// 下面代码的T都会报错
private static T intstance;
public static T getInstance() {
   return null;
}

// 泛型方法除外
public static <T>T getInstance() {
   return null;
}

这个跟JVM中的编译顺序有关系,因为编译时是先编译static的,但是泛型是要在使用的时候才知道是什么类型,这样一来就没办法在编译static的时候知道类型,所以会报错。

不能创建参数化类型的数组

泛型不能用于实例化类型变量

不能捕获泛型类的实例:泛型类不能继承 Exception和Throwable

正确的使用是:

private <T extends Exception> void doWork(T t) throws T {
    try {
    } catch (Exception e) {
        throw t;
    }
}

泛型类型的继承规则

有几个类存在这样的继承关系:

其中Dog 继承自Animal,但是 MyClass<Dog> 并没有继承自 MyClass<Animal>,

但是泛型类可以存在继承关系使用:

通配符类型

<? extends T>

以上方法在执行喂养Animal时是正常的,但是喂养Dog的时候就报错了,不让养,为了解决这种问题,就可以使用上界通配符,T是类型的上界,类型的参数是T的子类,使用方法如下:

看一下我们的MyClass,是一个简单的泛型类:

public class MyClass<T> {
    public T data;
    public T getData() {
        return this.data;
    }
    public void setData(T t) {
        this.data = t;
    }
}

我们在调用get 和set方法的时候会发现:

extends通配符,只允许get操作,不允许set操作,因此可以安全地获取数据,获取数据时要注意返回值类型:

? extends T  表示类型的上界,类型参数是T的子类,get方法返回的一定是个T(不管是T或者T的子类)编译器是可以确定知道的。但是set方法只知道传入的是个T,至于具体是T的那个子类编译器并不知道。

<? super T>

为了解决不能写入的问题,引入了super通配符,直接看案例:

使用时需要注意,set的时候传的参数需要是T及其字类:

? super  T 表示类型的下界,类型参数是T的父类(包括T本身),get方法返回的一定是个T的超类,那么到底是哪个父类?不知道,但是Object一定是它的超类,所以get方法返回Object,编译器是可以确定知道的。对于set方法来说,编译器不知道它需要的确切类型,但是T和T的子类可以安全的转型为T。

类型擦除

c# 和Java 分别如何实现泛型呢?

C#里面泛型无论在程序源码中、编译后的IL中(Intermediate Language,中间语言,这时候泛型是一个占位符),或是运行期的CLR中,都是切实存在的,List<int>与List<String>就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型。

Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的Java语言来说,ArrayList<int>与ArrayList<String>就是同一个类,所以泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。擦除法所谓的擦除,仅仅是对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。

### 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(); } ``` 这种设计模式常用于链式调用和构建器模式中,确保返回的类与当前类一致。 --- ###
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值