JAVA回忆录之泛型篇

2024-02-09 07:08
文章标签 java 回忆录 之泛

本文主要是介绍JAVA回忆录之泛型篇,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

泛型是什么

泛型是JDK1.5版本中加入的,在没有泛型之前,从集合中读取到的每一个对象都必须进行转化。如果有人不小心插入了类型错误的对象,在运行时的转化处理就会出错。有了泛型之后,可以告诉变一起每个集合中接受那些对象类型。编译器自动地为你的插入进行转化,并在编译时告知是否插入了类型错误的对象。

泛型最精准的定义:参数化类型。具体点说就是处理的数据类型不是固定的,而是可以作为参数传入。定义泛型类、泛型接口、泛型方法,这样,同一套代码,可以用于多种数据类型。

  • K ——键,比如映射的键。
  • V ——值,比如 List 和 Set 的内容,或者 Map中的值。
  • E ——异常类。
  • T ——泛型。

数组是协变的(协变:其实只是表示如果Stub为Super的子类型,那么类型Stub[]就是Super[]的子类型),泛型不是协变的。因此数组和泛型不能好好地混合使用。

文章目录

  • 泛型是什么
  • 泛型类、接口和泛型方法
    • 泛型类、接口
    • 泛型方法
  • 泛型擦除
  • 有界泛型类型
  • 泛型通配符参数
    • 有界通配符(上界)
    • 有界通配符(下界)
  • 存取原则和PECS法则
  • 泛型类的层次问题
    • 泛型类可以是类层次的一部分,就像非泛型类那样,因此,泛型类可以作为超类或子类。泛型和非泛型层次之间的关键区别是:在泛型层次中,类层次中的所有子类都必须向上传递超类所需要的所有类型参数。这与必须沿着类层次向上构造函数的参数类似。 使用泛型超类
    • 使用泛型子类
    • 强制转化
    • 重写泛型类的方法
    • 擦除
    • 模糊性错误
  • 泛型在使用过程中应该注意的问题
    • 不能用基本类型实例化类型参数
    • 不能抛出也不能捕获泛型类实例
    • 数据不能结合泛型使用
    • 不能实例化类型变量
    • 对静态成员的一些限制

泛型类、接口和泛型方法

泛型类、接口

public interface Iterable<T> {Iterator<T> iterator();default void forEach(Consumer<? super T> action) {Objects.requireNonNull(action);for (T t : this) {action.accept(t);}}default Spliterator<T> spliterator() {return Spliterators.spliteratorUnknownSize(iterator(), 0);}
}

一般而言,声明泛型接口的方式与声明泛型类相同。

泛型方法

public class Collections {/***其他代码省略***/public static <T> void sort(List<T> list, Comparator<? super T> c) {list.sort(c);}/***其他代码省略***/
}

泛型擦除

public class Main {public static void main(String[] args) {Generic<Integer> generic = new Generic<>();generic.set(new Integer(1));System.out.println(generic.get().intValue());}public static class Generic<T extends Number> {private T value;public T get() {return value;}public void set(T value) {this.value = value;}}
}

这是一个泛型使用的列子,我们反编译查看它的class文件:

Compiled from "Main.java"
public class com.loadclass.generic.Main$Generic<T extends java.lang.Number> {public com.loadclass.generic.Main$Generic();Code:0: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnpublic T get();Code:0: aload_01: getfield      #2                  // Field value:Ljava/lang/Number;4: areturnpublic void set(T);Code:0: aload_01: aload_12: putfield      #2                  // Field value:Ljava/lang/Number;5: return
}public class com.loadclass.generic.Main {public com.loadclass.generic.Main();Code:0: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: new           #2                  // class com/loadclass/generic/Main$Generic3: dup4: invokespecial #3                  // Method com/loadclass/generic/Main$Generic."<init>":()V7: astore_18: aload_19: new           #4                  // class java/lang/Integer12: dup13: iconst_114: invokespecial #5                  // Method java/lang/Integer."<init>":(I)V17: invokevirtual #6                  // Method com/loadclass/generic/Main$Generic.set:(Ljava/lang/Number;)V20: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;23: aload_124: invokevirtual #8                  // Method com/loadclass/generic/Main$Generic.get:()Ljava/lang/Number;27: checkcast     #4                  // class java/lang/Integer30: invokevirtual #9                  // Method java/lang/Integer.intValue:()I33: invokevirtual #10                 // Method java/io/PrintStream.println:(I)V36: return
}

从class文件中我们可以看出来,其实泛型在编译后都进行了擦除,类型T都转化为了它的超类Number,然后在需要使用的时候进行checkcast。当然在泛型没有做任何显示的时候比如Generic,这样在编译生成的class文件中T都是转化为Object类型来处理的。

有界泛型类型

泛型参数类型可以使用任意参数类型替换。对于大多数情况这很好,但是限制能够传递给类型参数的类型是有时有用的。例如,假设希望创建一个泛型类,类中返回数据中数据平均值的方法(类型数字包括:整数、单精度和双精度)。

public class Stats<T extends Number> {T[] nums;public Stats(T[] nums) {this.nums = nums;}double average() {double sum = 0.0;for (int i = 0; i < nums.length; i++)sum += nums[i].doubleValue();return sum / nums.length;}
}

向上边代码我们可以使用Number对T类型做限制,这样所有T类型对象都可以调用doubleValue()方法,因为该方法是由Number声明的。

泛型通配符参数

先看一段代码:

class Stats<T extends Number> {T[] nums;public Stats(T[] nums) {this.nums = nums;}double average() {double sum = 0.0;for (int i = 0; i < nums.length; i++)sum += nums[i].doubleValue();return sum / nums.length;}boolean sameAge(Stats<T> ob) {if (average() == ob.average()) {return true;}return false;}
}

这样的实现导致只有sameAge方法的参数类型和地啊用对象的类型相同时才能工作。

为了创建smaeAvg方法,必须使用Java泛型的另一个特性:通配符参数。通配符参数是由“?”指定的,表示未知类型。

class Stats<T extends Number> {T[] nums;public Stats(T[] nums) {this.nums = nums;}double average() {double sum = 0.0;for (int i = 0; i < nums.length; i++)sum += nums[i].doubleValue();return sum / nums.length;}boolean sameAge(Stats<> ob) {if (average() == ob.average()) {return true;}return false;}
}

此时,Stats<?>和所有的Stats对象匹配,允许任意两个Stats对象比较它们的平均值。

在讲述有界通配符之前我们先看一段代码:

Apple[] apples = new Apple[1];
Fruit[] fruits = apples;
fruits[0] = new Strawberry();

因为数组可以协变的,所以Apple的数据applse可以赋值给子类数组fruits,但是在给数组中的某个Fruit对象赋值的时候假如不是Apple或者其子类类型,那么代码可以编译,但在允许时会抛出java.lang.ArrayStoreException的异常。

有界通配符(上界)

向上造型一个泛型对象的引用<? extends superclass>

我们可以使用通配符把相关的代码转换程泛型:因为Apple是Fruit的一个子类,我们使用? extends 通配符,这样就能将一个List对象的定义赋到一个List<? extends Fruit>的声明上:

List<Apple> apples = new ArrayList<Apple>();
List<? extends Fruit> fruits = apples;

这次,代码就编译不过去了!Java编译器会阻止你往一个Fruit list里加入strawberry。在编译时我们就能检测到错误,在运行时就不需要进行检查来确保往列表里加入不兼容的类型了。即使你往list里加入Fruit对象也不行:

fruits.add(new Fruit());

事实上你不能够往一个使用了? extends的数据结构里写入任何的值。

原因非常的简单,你可以这样想:这个? extends T 通配符告诉编译器我们在处理一个类型T的子类型,但我们不知道这个子类型究竟是什么。因为没法确定,为了保证类型安全,我们就不允许往里面加入任何这种类型的数据。另一方面,因为我们知道,不论它是什么类型,它总是类型T的子类型,当我们在读取数据时,能确保得到的数据是一个T类型的实例:

Fruit get = fruits.get(0);

有界通配符(下界)

向下造型一个泛型对象的引用<? super superclass>

使用<? super superclass>通配符一般是什么情况?让我们先看看这个:

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

我们看到fruits指向的是一个装有Apple的某种超类(supertype)的List。同样的,我们不知道究竟是什么超类,但我们知道Apple和任何Apple的子类都跟它的类型兼容。既然这个未知的类型即是Apple,也是GreenApple的超类,我们就可以写入:

fruits.add(new Apple());
fruits.add(new GreenApple());

如果我们想往里面加入Apple的超类,编译器就会警告你:

fruits.add(new Fruit());
fruits.add(new Object());

因为我们不知道它是怎样的超类,所有这样的实例就不允许加入。

从这种形式的类型里获取数据又是怎么样的呢?结果表明,你只能取出Object实例:因为我们不知道超类究竟是什么,编译器唯一能保证的只是它是个Object,因为Object是任何Java类型的超类。

存取原则和PECS法则

请记住PECS法则:生产者(Producer)使用extends,消费者(Consumer)使用super。

  • 生产者使用extends

如果你需要一个列表提供T类型的元素(即你想从列表中读取T类型的元素),你需要把这个列表声明成<? extends T>,比如List<? extends Integer>,因此你不能往改列表中添加任何元素。

  • 消费者使用super

如果需要一个列表使用T类型的元素(即你想把T类型的元素加入到列表中),你需要把这个列表声明成<? super T>,比如List<? super Integer>,因此你不能保证从中读取到的元素的类型。

  • 即是生产者,也是消费者

如果一个列表既要生成,又要消费,你不能使用泛型通配符声明列表,比如List。

泛型类的层次问题

泛型类可以是类层次的一部分,就像非泛型类那样,因此,泛型类可以作为超类或子类。泛型和非泛型层次之间的关键区别是:在泛型层次中,类层次中的所有子类都必须向上传递超类所需要的所有类型参数。这与必须沿着类层次向上构造函数的参数类似。
使用泛型超类

class Gen<T> {private T obj;Gen(T object) {this.obj = object;}public T get() {return obj;}
}

Generic继承Gen:

public class Generic<T> extends Gen<T> {Generic(T object) {super(object);}
}

继承泛型类的子类也可以拥有自己的泛型:

public class Generic<V,T> extends Gen<T> {private V value;Generic(T object, V value) {super(object);this.value = value;}public V getValue() {return value;}
}

使用泛型子类

class NoGen{int num;NoGen(int num) {this.num = num;}int getNum(){return num;}
}

子类继承NoGen并实现泛型:

class Gen<T> extends NoGen {private T obj;Gen(T object, int value) {super(value);this.obj = object;}public T get() {return obj;}
}

强制转化

只有当两个泛型类型实例的类型相互兼容并且他们的类型参数也是相同时,才能将其中的一个实例转化为另一个实例:

List<Integer> listInteger = new ArrayList<Integer>();//legal
List<Integer> listLong = new ArrayList<Long>();//illegal

重写泛型类的方法

可以像重写其他任何方法那样重写泛型类的方法:

class Gen<T>{T obj;Gen(T object) {this.obj = object;}public T get() {return obj;}
}
public class Generic<T> extends Gen<T> {Generic(T object) {super(object);}public T get() {obj = null;return obj;}
}
  • 泛型类型推断
List<Integer> list = new ArrayList<Integer>();//<1.7
List<Integer> integerList = new ArrayList<>();//1.7

在JDK1.6的时候我们声明泛型和new一个泛型实例时必须制定相同的类型。而从
JDK1.7开始new的泛型实例不用制定类型,编译期会默认与声明的对象用于相同的泛型类型。

擦除

前文中讲过泛型的擦除,为什么这里还需要再讲述呢?这里讲述在继承泛型时的擦除,仔细阅读会有不一样的发现哦~!

class Gen<T>{T obj;Gen(T object) {this.obj = object;}public T get() {return obj;}
}
public class Generic extends Gen<String> {Generic(String object) {super(object);}public String get() {return obj;}
}

我们分析编译后生成的Generic的class文件:

Compiled from "Generic.java"
public class com.genericity.Generic extends com.genericity.Gen<java.lang.String> {com.genericity.Generic(java.lang.String);Code:0: aload_01: aload_12: invokespecial #1                  // Method com/genericity/Gen."<init>":(Ljava/lang/Object;)V5: returnpublic java.lang.String get();Code:0: aload_01: getfield      #2                  // Field obj:Ljava/lang/Object;4: checkcast     #3                  // class java/lang/String7: areturnpublic java.lang.Object get();Code:0: aload_01: invokevirtual #4                  // Method get:()Ljava/lang/String;4: areturn
}

我们可以看出Generic中有两个get方法,这是编译期偶尔需要为类添加桥接方法。

桥接方法
子类中重写方法的类型擦除不能产生于超类中方法相同的擦除。对于这种情况,会生成使用超类类型擦除的方法,并且这个方法调用具有由子类指定的类型擦除的方法。当然桥接方法只会在字节码级别发生。

模糊性错误

泛型的引入,增加了引起一种新类型错误——模糊性错误的可能,必须注意防范。当擦除导致两个看起来不同的泛型声明,在擦除后变成相同的类型而导致冲突时,就会发生模糊性错误。

class Gen<T>{private T obj;public void set(T object) {this.obj = object;}public T get() {return obj;}
}
public class Generic<V, T> extends Gen<T> {V value;public void set(V value) {//illegalthis.value = value;}
}

这样进行重载防范的时候,如果进行泛型擦除那么两个方法一模一样。像这样的情况使用两个独立的方法名会更好一些,而不是试图重载set方法。

泛型在使用过程中应该注意的问题

不能用基本类型实例化类型参数

Map<int, int> pair = new HashMap<int, int>();

这样的语句是非法的。

不能抛出也不能捕获泛型类实例

泛型类扩展Throwable即为不合法,因此无法抛出或捕获泛型类实例。但在异常声明中使用类型参数是合法的:

public static <T extends Throwable> void doWork(T t) throws T {try {...} catch (Throwable realCause) {t.initCause(realCause);throw t;}
}

数据不能结合泛型使用

在Java中数据是协变的,Object[]数组可以是任何数组的父类(因为任何一个数组都可以向上转型为它在定义时指定元素类型的父类的数组)。

String[] strs = new String[10];
Object[] objs = strs;
objs[0] = new Long(1);

在上述代码中,我们将数组元素赋值为满足父类(Object)类型,但不同于原始类型Long的对象,在编译时能够通过,而在运行时会抛出ArrayStoreException异常。

不能实例化类型变量

不能以诸如“new T(…)", “new T[…]”, "T.class"的形式使用类型变量。Java禁止我们这样做的原因很简单,编译期不知道创建那种类型的对象。T只是一个占位符。

对静态成员的一些限制

注意,这里我们强调了泛型类。因为普通类中可以定义静态泛型方法,如上面我们提到的ArrayAlg类中的getMiddle方法。关于为什么有这样的规定,请考虑下面的代码:

public class People<T> { public static T name; public static T getName() { ... }
}

我们知道,在同一时刻,内存中可能存在不只一个People类实例。假设现在内存中存在着一个People对象和People对象,而类的静态变量与静态方法是所有类实例共享的。那么问题来了,name究竟是String类型还是Integer类型呢?基于这个原因,Java中不允许在泛型类的静态上下文中使用类型变量。

泛型在我们编码的过程中,特别是写一些框架或者通用组件时是非常有帮助的。

文章到这里就全部讲述完啦,若有其他需要交流的可以留言哦

想阅读作者的更多文章,可以查看我 个人博客 和公共号:
振兴书城

这篇关于JAVA回忆录之泛型篇的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/693483

相关文章

Java五子棋之坐标校正

上篇针对了Java项目中的解构思维,在这篇内容中我们不妨从整体项目中拆解拿出一个非常重要的五子棋逻辑实现:坐标校正,我们如何使漫无目的鼠标点击变得有序化和可控化呢? 目录 一、从鼠标监听到获取坐标 1.MouseListener和MouseAdapter 2.mousePressed方法 二、坐标校正的具体实现方法 1.关于fillOval方法 2.坐标获取 3.坐标转换 4.坐

Spring Cloud:构建分布式系统的利器

引言 在当今的云计算和微服务架构时代,构建高效、可靠的分布式系统成为软件开发的重要任务。Spring Cloud 提供了一套完整的解决方案,帮助开发者快速构建分布式系统中的一些常见模式(例如配置管理、服务发现、断路器等)。本文将探讨 Spring Cloud 的定义、核心组件、应用场景以及未来的发展趋势。 什么是 Spring Cloud Spring Cloud 是一个基于 Spring

Javascript高级程序设计(第四版)--学习记录之变量、内存

原始值与引用值 原始值:简单的数据即基础数据类型,按值访问。 引用值:由多个值构成的对象即复杂数据类型,按引用访问。 动态属性 对于引用值而言,可以随时添加、修改和删除其属性和方法。 let person = new Object();person.name = 'Jason';person.age = 42;console.log(person.name,person.age);//'J

java8的新特性之一(Java Lambda表达式)

1:Java8的新特性 Lambda 表达式: 允许以更简洁的方式表示匿名函数(或称为闭包)。可以将Lambda表达式作为参数传递给方法或赋值给函数式接口类型的变量。 Stream API: 提供了一种处理集合数据的流式处理方式,支持函数式编程风格。 允许以声明性方式处理数据集合(如List、Set等)。提供了一系列操作,如map、filter、reduce等,以支持复杂的查询和转

Java面试八股之怎么通过Java程序判断JVM是32位还是64位

怎么通过Java程序判断JVM是32位还是64位 可以通过Java程序内部检查系统属性来判断当前运行的JVM是32位还是64位。以下是一个简单的方法: public class JvmBitCheck {public static void main(String[] args) {String arch = System.getProperty("os.arch");String dataM

详细分析Springmvc中的@ModelAttribute基本知识(附Demo)

目录 前言1. 注解用法1.1 方法参数1.2 方法1.3 类 2. 注解场景2.1 表单参数2.2 AJAX请求2.3 文件上传 3. 实战4. 总结 前言 将请求参数绑定到模型对象上,或者在请求处理之前添加模型属性 可以在方法参数、方法或者类上使用 一般适用这几种场景: 表单处理:通过 @ModelAttribute 将表单数据绑定到模型对象上预处理逻辑:在请求处理之前

eclipse运行springboot项目,找不到主类

解决办法尝试了很多种,下载sts压缩包行不通。最后解决办法如图: help--->Eclipse Marketplace--->Popular--->找到Spring Tools 3---->Installed。

JAVA读取MongoDB中的二进制图片并显示在页面上

1:Jsp页面: <td><img src="${ctx}/mongoImg/show"></td> 2:xml配置: <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001

Java面试题:通过实例说明内连接、左外连接和右外连接的区别

在 SQL 中,连接(JOIN)用于在多个表之间组合行。最常用的连接类型是内连接(INNER JOIN)、左外连接(LEFT OUTER JOIN)和右外连接(RIGHT OUTER JOIN)。它们的主要区别在于它们如何处理表之间的匹配和不匹配行。下面是每种连接的详细说明和示例。 表示例 假设有两个表:Customers 和 Orders。 Customers CustomerIDCus

22.手绘Spring DI运行时序图

1.依赖注入发生的时间 当Spring loC容器完成了 Bean定义资源的定位、载入和解析注册以后,loC容器中已经管理类Bean 定义的相关数据,但是此时loC容器还没有对所管理的Bean进行依赖注入,依赖注入在以下两种情况 发生: 、用户第一次调用getBean()方法时,loC容器触发依赖注入。 、当用户在配置文件中将<bean>元素配置了 lazy-init二false属性,即让