Java 中什么是泛型 为什么会有泛型的出现

Java 投稿 126100 0 评论

Java 中什么是泛型 为什么会有泛型的出现

什么是泛型

泛型(Generic),Generic 的意思有「一般化的,通用的」。

这里还涉及到一个词「参数化类型」。什么意思呢?

把类型参数化(只能感慨中国文化博大精深),即我们可以把类型作为参数,换句话说,就是所操作的数据类型被指定为一个参数。

同理,类型,即 Java 中的各种基本的引用类型,当然包含你自己定义的类型,说白了就是各种类(Class),类可以作为参数,就是上面讲的把类型作为参数(好吧,好像讲了一堆废话)。这又涉及到一个词,即「类型参数」。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    ...
}

中的,这里的  可以说是一个「类型形参」。

实参」。

简而言之:

类型形参; 中的  称为 类型实参。这两个合起来,就是上面提到的「类型参数」。

为什么会有泛型的出现

泛型集合」。

List<Integer> list = new ArrayList<>(); // 泛型集合

当然,我们一开始学习的时候,并没有用到泛型,即非泛型集合。

List list = new ArrayList();	// 非泛型集合

在以前没有泛型的情况下,我们看看会出现什么问题。默认 ArrayList 集合中存储的元素类型是  ,这样很好,Java 中任何类型的终极父类就是 Object,什么类型的数据都能存储到这个集合中。

List list = new ArrayList();	// 非泛型集合
list.add("Hello World!");		// 存储 String 类型
list.add(23);					// 存储 int 类型,这里会自动装箱为 Integer 类型
list.add(true);					// 存储 Boolean 类型

for (Object o : list) {			// 用 Object 接收,合情合理
    System.out.println(o);
}

我们存储数据之后,后续肯定需要使用它,就需要从集合中取出来,而取出来进一步操作是需要明确具体的数据类型,那么就需要进行强制类型转换。

for (Object o : list) {
    String s = (String) o;		// 强制类型转换
    // 后续操作...
}

此时代码并不会报错,编译也不会有问题,直到我们运行时,就会出现异常——。

看到这里,估计有小伙伴要问了,我一个一个强制转换不行吗?我知道存储的是什么数据,到时直接获取相对应的数据进行强转就行了啊。是,没错,你可以一个一个强转,数量少的情况下是可以,但是你数量很多的情况下呢?你怎么办?

List<String> list = new ArrayList<>();	// 泛型集合
list.add("Hello World!");				// 存储 String 类型
list.add("23");
list.add("Coding Coding");

for (String s : list) {					// 用 String 接收
    System.out.println(s);
    // 后续操作...
}

看到这里的小伙伴,可能有这么一个疑惑:那这样为什么不直接使用一个 String 数组呢?这个问题问得好。

这里使用了泛型,那么我们在  的时候,编译期间就会对添加的元素进行类型检查,而且在获取集合元素的时候,也不需要强制类型转换了,直接用指定的类型接收就行了。

使用泛型有什么好处

  • 无需强制类型转换(集合、反射)

  • 增加代码可读性,我们可以通过泛型,知道现在操作的是什么数据类型。一句话,给人看的。

  • 代码复用,可以根据不同情况传入不同的数据类型,进行不同的操作。

泛型类

定义语法:

class 类名<通配符,通配符,通配符...> {
    private 通配符 变量名;
    ...
}

通配符:T、E、K、V,也就是上面说的类型形参。(这里的通配符,也有人称为泛型标识)

使用语法:

类名<具体的数据类型> 对象名 = new 类名<具体的数据类型>();
类名<具体的数据类型> 对象名 = new 类名<>();		// JDK 7 开始可以省略,人们称为 菱形语法

举个栗子:

/**
	定义泛型类
*/
public class Generic<T> {
    private T variable;
        
    public void setVariable(T variable) {
        return this.variable = variable;
    }
    
    public T getVariable() {
        return variable;
    }
}

测试

Generic<String> g = new Generic<>();	// 指定泛型为 String
g.setVariable("god23bin");				// 正常
g.setVariable(23);						// 提示错误,因为这里是 int 型
String var = g.getVariable();

需要注意的点:

  • 泛型的类型参数,只能是引用数据类型,不支持基本数据类型。

  • 类型擦除」)

// 子类如果需要是泛型类,那么其类型参数需要包含父类的类型参数
class ChildGeneric<T> extends Generic<T> {}		// OK
class ChildGeneric<T, E> extends Generic<T> {}	// OK

// 子类不是泛型类,那么父类的类型参数需要明确
class ChildGeneric extends Generic<String> {}

泛型接口

定义语法:

interface 接口名 <通配符,通配符,通配符...> {
    通配符 方法名();
    ...
}

使用语法:

// 接口实现类是泛型类,那么实现类的类型参数需要包含接口的类型参数
class Demo<T> implements Generic<T> {}    // OK
class Demo<T, E> implements Generic<T> {} // OK

// 接口实现类不是泛型类,那么接口类型参数需要明确
class Demo implements Generic<String> {}

泛型方法

之前是在类和接口上定义了泛型,然而有时候,我们并不需要整个类都定义类型,只需要其中某一个方法定义泛型,只关心这一个方法,这时就可以使用把泛型定义在方法上,这样调用泛型方法的时候,才指定具体的类型参数。

定义语法:

访问修饰符 <通配符,通配符,通配符...> 返回值类型 方法名(形参列表) {
    // 方法体
}

举个栗子:

public <T, E> void getGeneric() {
    // 方法体
}

public <T, E> void getGeneric(Game<T> game) {
    // 方法体
}

这里需要注意的是,泛型方法和泛型类中使用了泛型的普通的方法是不一样的。

// 这是泛型类中使用了泛型的普通的方法
public T getVariable() {
    return variable;
}

// 这是泛型方法,只有定义了 <T,...> 的方法才是泛型方法
public <T, E> void getGeneric() {
    // 方法体
}

而且,如果你在泛型类中定义了泛型方法,那么泛型方法中的  类型形参和泛型类上的类型形参是不一样的,是相互独立的。还有,泛型方法可以定义成静态的,还没完,泛型方法还可以结合可变参数。

举个栗子:

/**
	定义泛型类
*/
public class Generic<T> {
    private T variable;
        
    public void setVariable(T variable) {
        return this.variable = variable;
    }
    
    public T getVariable() {
        return variable;
    }
    
    // 泛型方法,这里的T和类上的T不是同一个T
    public <T> T getGeneric(List<T> list) {
        return list.get(0);
    }
    
    // 静态的泛型方法
    public static <T> T getGenericStatic(List<T> list) {
        return list.get(0);
    }
    
    // 结合可变参数的泛型方法
    public static <E> void print(E... e) {
        // 这里的参数可以当作数组进行遍历
        for (E elem : e) {
            System.out.println(elem);
        }
    }
}

通配符之问号

之前出现的  这些,也都是通配符,不过,这些通配符是属于类型形参的通配符。那么类型实参的通配符呢?这就来啦!类型实参通配符:。没错,你没看错,就是一个问号。

任意类型。

举个例子:

public class Generic<T> {
    ...
    
    public static void showGame(Games<String> games) { // 要求Games指定的类型为String
        String one = games.getOne();
        System.out.println(one);
    }
    
}

上面要求 Games 指定的类型为 String。那么我们这样操作:

Generic<String> g = new Generic<>();

Games<String> games = new Games();
g.showGame(games);		// OK

Games<Integer> games2 = new Games();
g.showGame(games2);		// Error,因为指定了为String

所以使用  通配符

public class Generic<T> {
    ...
    
    public static void showGame(Games<?> games) { // 使用类型实参通配符 ?
        String one = games.getOne();
        System.out.println(one);
    }
    
}

通配符上下限

类型通配符的上下限,有的地方也称为上下界,还有称限定通配符的,意思都一样。

上限语法:

类/接口<? extends 实参类型>

这里的 extends 可以这样理解,,使用的时候,我们传入的实参类型需要小于等于A类,即需要是A的子类或A本身,这样就限制了通配符的上限了,你最高只能是A类。

下限语法:

类/接口<? super 实参类型>

这里的 super 可以这样理解,,使用的时候,我们传入的实参类型需要大于等于A类,即需要是A的父类或A本身,这样就限制了通配符的下限了,你最低只能是A类。

public class Demo {
    public static upperLimit(List<? extends B> list) { // 类型实参通配符上限为B类
        // ...
    }
    
    public static lowerLimit(List<? super B> list) { // 类型实参通配符下限为B类
        // ...
    }
}

调用这个方法

List<A> l1 = new ArrayList<>();
List<B> l2 = new ArrayList<>();
List<C> l3 = new ArrayList<>();

Demo.upperLimit(l1);	// Error,这里传入 l1,而上面搞了通配符上限,超过了B类,比B类还上
Demo.upperLimit(l2);	// OK
Demo.upperLimit(l3);	// OK

Demo.lowerLimit(l1);	// OK
Demo.lowerLimit(l2);	// OK
Demo.lowerLimit(l3);	// Error,同理,比B类还下,自然错误,需要比B类上,超过B类才行

需要注意的是,你搞了通配符的上限,在集合中,那么是只能用来读取数据,而不能用来存储数据,这该怎么理解呢?

public class Demo {
    public static upperLimit(List<? extends B> list) { 	// 类型实参通配符上限为B类
        list.add(new B()); // Error
        list.add(new C()); // Error
        // 因为我们使用上限通配符,不知道传入进来的 List 是什么类型的,可能是List<B>,可能是List<C>
        // 所以是不能进行存储数据的
    }
    
    public static lowerLimit(List<? super B> list) { 	// 类型实参通配符下限为B类
        // ...
    }
}

那么下限呢?放心,下限没有这个问题,可以存储数据。

public class Demo {
    public static upperLimit(List<? extends B> list) { 	// 类型实参通配符上限为B类
        // ...
    }
    
    public static lowerLimit(List<? super B> list) { 	// 类型实参通配符下限为B类
        list.add(new B());
        list.add(new C());
        // 因为下限通配符,只限定了下限,但是上限是没有限制的,也就是说可以看成上限就是 Object
        // 上限是 Object,那么任何类都默认继承 Object,那么自然可以添加 C 类型的数据
        // 也就是存储数据的类型是没有限制的。
        for (Object o : list) {
            System.out.println(o);
        }
    }
}

类型擦除

泛型的限制,只在编译期存在,一旦在运行了,那么便消失了,即类型被擦除了。

  • 无限制类型擦除

  • 有限制类型擦除

无限制:

Java 泛型是什么?常用的通配符有哪些?

编译时类型安全检测的机制。这个机制可以在编译时就检测到非法的数据类型。本质是一个参数化类型,就是所操作的数据类型可以被指定为一个特定的参数类型。

Java 的泛型是如何工作的 ? 什么是类型擦除?(泛型擦除是什么?)

Java 的泛型是伪泛型,因为在 Java 运行期间,这些泛型信息都会被擦掉,就是所谓的类型擦除(泛型擦除)。

什么是泛型中的限定通配符和非限定通配符?

一种是,它通过确保类型必须是T的子类来限定上界,即类型必须为T类型或者T子类

表示了非限定通配符,因为 < ? > 可以用任意类型来替代。

你的项目中哪里用到了泛型?

  • 可用于定义通用返回结果类  通过参数  可根据具体的返回类型动态指定结果的数据类型

  • 用于构建集合工具类。参考  中的 ,  方法

编程笔记 » Java 中什么是泛型 为什么会有泛型的出现

赞同 (69) or 分享 (0)
游客 发表我的评论   换个身份
取消评论

表情
(0)个小伙伴在吐槽