Java核心(三)——泛型和枚举
iwehdio的博客园:https://www.cnblogs.com/iwehdio/
学习自:
1、泛型
-
Java泛型设计原则:只要在编译时期没有出现警告,那么运行时期就不会出现ClassCastException异常。
-
ClassCastException异常:类型转换异常。
-
泛型:把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型。
-
参数化类型:
- 把类型当作是参数一样传递。
<数据类型>
只能是引用类型。
-
相关术语:
ArrayList<E>
中的E称为形式类型参数。ArrayList<Integer>
中的Integer称为实际类型参数。- 整个
ArrayList<E>
称为泛型类型。 - 整个
ArrayList<Integer>
称为参数化的类型ParameterizedType。
-
为什么需要泛型:
- 早期Java是使用Object来代表任意类型的,但是向下转型有强转的问题,这样程序就不太安全。
- 对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常。
- 泛型的好处就是在编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的。
- 有了泛型以后:
- 代码更加简洁【不用强制转换】。
- 程序更加健壮【只要编译时期没有警告,那么运行时期就不会出现ClassCastException异常】。
- 可读性和稳定性【在编写集合的时候,就限定了类型】。
-
泛型类:
- 泛型类就是把泛型定义在类上,用户使用该类的时候,才把类型明确下来。
- 类型变量定义在类上,方法中也可以使用。
- 用户想要使用哪种类型,就在创建的时候指定类型。使用的时候,该类就会自动转换成用户想要使用的类型了。
-
泛型方法:
- 如果仅在某一个方法上需要使用泛型,外界仅仅是关心该方法,不关心类其他的属性,就可以只在类上使用泛型。
- 需要先定义后使用
public <T> void show(T t)
。
-
泛型类被继承:
-
分为两种情况:子类明确泛型类的类型参数变量,子类不明确泛型类的类型参数变量。
-
子类明确泛型类的类型参数变量:
public class Inter<T>{} public class InterImpl extends Inter<String>{}
-
子类不明确泛型类的类型参数变量:
public class Inter<T>{} public class InterImpl<T> extends Inter<T>{}
- 当子类不明确泛型类的类型参数变量时,外界使用子类的时候,也需要传递类型参数变量进来,在实现类上需要定义出类型参数变量。
-
实现类的要是重写父类的方法,返回值的类型是要和父类一样的。
-
类上声明的泛形只对非静态成员有效。
-
-
类型通配符:
- 泛型中的
<Object>
并不是像以前那样有继承关系的,也就是说List<Object>
和List<String>
是毫无关系的。 - Java泛型提供了类型通配符
?
,?
通配符表示可以匹配任意类型,任意的Java类都可以匹配。 - 使用?号通配符的时候:就只能调对象与类型无关的方法,不能调用对象与类型有关的方法。
- 泛型中的
-
简单泛型为什么需要左右一致:
- 如果允许
List<Number>
指向new ArrayList<Integer>()
,由于简单泛型允许存入子类元素,最终List里会混入Integer、Long、Double等多种类型元素。然后等到new ArrayList<Integer>()
重新被赋值给List<Integer>
时就出事了: - 编译器看到
List<Integer>
,只会按它的边界类型转,也就是自动转型为Integer,此时会抛ClassCastException。
- 如果允许
-
设置通配符上下限:
- 设置上限:
- 如果想接收一个List集合,它只能操作数字类型的元素【Float、Integer、Double、Byte等数字类型都行】。
- 设定通配符上限
List<? extends Number>
,表示List集合装载的元素只能是Number的子类或自身。 - 泛型中可以使用 Number 的方法。
- 设置下限:
- 传递进来的只能是Type或Type的父类:
<? super Type>
。
- 传递进来的只能是Type或Type的父类:
- 无论是设定通配符?的上限还是下限,都是不能操作与对象有关的方法。
- 泛型中的参数不考虑其继承关系,如果要考虑其继承关系应该使用设置上下限的方法。
- 带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。
- 如果使用extends指明类型,List不允许通过
add(E e)
方法添加上限的的对象,唯一例外的是 null。 - 如果使用super指明类型,List通过
get()
方法取出数据只能往object自动转型。 - “存入”指的是形参带泛型的方法,而“取出”则是返回值带泛型的方法。
- 如果使用extends指明类型,List不允许通过
- 设置上限:
-
extends小结
- List<? extends Human>指向:只能指向子类型List,比如
List<Chinese>
,Human是最上边的类。 - List<? extends Human>取出:接上一条限制,不论指向什么List元素必然是Human及其子类型,按Human转。
- List<? extends Human>存入:简单泛型要是能解决早解决了,还轮得到我?直接禁止存入。
- 频繁往外读取内容的,适合用<? extends T>:extends返回值稍微精确些。
- List<? extends Human>指向:只能指向子类型List,比如
-
super小结
- List<? super Human>接收:只能指向父类型List,比如
ArrayList<Creature>
、ArrayList<Primate>
。 - List<? super Human>取出:只能转Object。
- 通过List<? super Human>取值时,由于它可能指向
ArrayList<Primate>
也可能指向ArrayList<Creature>
,都是Human的父类型,而且Human的父类型未来还可能增加。 - Java所有类型都继承自Object,所以用Object接收万无一失。
- 通过List<? super Human>取值时,由于它可能指向
- List<? super Human>存入:只能存Human及其子类型元素。
- 经常往里插入的,适合用<? super T>:super允许存入子类型元素。
- List<? super Human>接收:只能指向父类型List,比如
-
?小结
- 存:禁止存入
- 取:只能是Object
-
T和?的区别:
- T是一个确定的类型(类型参数),通常用于泛型类和泛型方法的定义,?是一个 不确定的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。
- 通过T可以确保泛型参数的一致性,而?不能。
- 类型参数T可以多重限定(用&设置多个上限)而通配符?不行。
- 通配符?可以使用超类限定(设置下界)而类型参数T不行。
- ?可作为实例化对象时的类型参数。
-
如何选择是用通配符还是泛型方法:
-
大多时候,我们都可以使用泛型方法来代替通配符的:
//使用通配符 public static void test(List<?> list) { } //使用泛型方法 public <T> void test2(List<T> t) { }
-
如果参数之间的类型有依赖关系,或者返回值是与参数之间有依赖关系的。那么就使用泛型方法。
-
如果没有依赖关系的,就使用通配符,通配符会灵活一些。
-
-
泛型擦除:
- 泛型是提供给javac编译器使用的,它用于限定集合的输入类型,让编译器在源代码级别上,即挡住向集合中插入非法数据。
- 但编译器编译完带有泛形的java程序后,生成的class文件中将不再带有泛形信息,以此使程序运行效率不受到影响,这个过程称之为“擦除”。
- 因为有类型擦除的存在,不能通过泛型的不同来重载方法。
-
泛型的兼容性:
- JDK5提出了泛型这个概念,但是JDK5以前是没有泛型的。也就是泛型是需要兼容JDK5以下的集合的。
- 当把带有泛型特性的集合赋值给老版本的集合时候,会把泛型给擦除了。它保留的就类型参数的上限Object。
- 如果把没有类型参数的集合赋值给带有类型参数的集合赋值,不会报错,仅仅是提示“未经检查的转换”。
2、泛型实现机制
-
运行Java代码的两个阶段:编译期、运行期。
-
Java代码运行有4个要素:源代码、编译器、字节码、虚拟机。
-
泛型的实现:
- 为了兼容各种类型,泛型底层还是采用Object接收所有类型。ArrayList
也一样,它其实底层还是Object[]存储元素。 - Java的泛型存在“编译后泛型擦除”。
- 而编译器自动类型转换也和“泛型擦除”有关:因为底层还是Object接收,要想返回实际类型,只能强转。
- 泛型是JDK专门为编译器创造的语法糖,只在编译期,由编译器负责解析,虚拟机不知情。
- 存入:普通类继承泛型类并给变量类型T赋值后,就能强制让编译器帮忙进行类型校验。
- 取出:代码编译时,编译器底层会根据实际类型参数自动进行类型转换,无需程序员在外部手动强转。
- 通过反射,是可以绕过编译器对泛型的检查,存入其他类型的对象。
- 为了兼容各种类型,泛型底层还是采用Object接收所有类型。ArrayList
-
JDK本质是想编写一套代码模板,泛型是实现代码模板的一种手段,而多态则是代码模板的底层支持。要实现代码模板,编译期的泛型约束和运行期的多态支持缺一不可。
-
代码模板的本质就是:用Object接收一切对象,用泛型+编译器限定特定对象,用多态支持类型强转。
-
泛型有以下作用:
- 抽取代码模板:代码复用并且可以通过指定类型参数与编译器达成约定
- 类型校验:编译时阻止不匹配元素进入Object[]
- 类型强转:根据泛型自动强转(多态,向下转型)
- 实际运行时,Object[]接收特定类型的元素,体现了多态,取出Object[]元素并强转,也体现了多态。
-
泛型为什么不支持基本数据类型:
- 本身泛型的引入就是为了解决引用类型强转易出错的问题,也就自然不会去考虑基本类型。
- JDK1.5不仅引入泛型,还同时发布自动拆装箱特性,所以int完全可以用Integer代替,也就无需支持int。
- Java已经尽自己最大的努力让泛型支持基本类型了。只不过它不是从语法上支持,而是从功能上支持。
- 但归根结底,Java泛型之所以无法支持基本类型,还是因为存在泛型擦除,底层仍是Object,而基本类型无法直接赋值给Object类型,导致JDK只能用自动拆装箱特性来弥补,而自动拆装箱会带来性能损耗。
3、枚举
-
枚举是一种特殊的类,和class、interface等是一个级别的(其实就是一个类),一般用于表示多种固定的状态。
-
自定义类山寨枚举:
- 静态代码块初始化final常量。
- 构造器私有,防止外部创建。
-
正版枚举:
-
反编译后:
-
与山寨"枚举"的区别在于:
- enum关键字修饰的WeekDayEnum,在编译后会自动继承Enum类,且声明为final
- 比山寨版枚举多了valueOf()方法,传入名称返回对应的枚举实例
- values()并不是直接返回WeekDay[],而是$VALUES.clone()
- 防止外部调用者随意增删枚举对象。
- 正版枚举重写了toString()方法。
- 然当初编写WeekDayEnum时,构造器只传入了两个参数,但编译器却给我们额外加了两个,用来为父类Enum中的name和ordinal赋值。name来自MONDAY、TUESDAY等枚举名称,而ordinal则是编写序号。
-
枚举和常量类有什么区别呢?
- 作为常量使用时,区别不大。
- 枚举优点一:限制重复定义常量。枚举作为常量使用时,编译器会自动限制取值不能相同,而常量类做不到,有可能会重复。
- 枚举优点二:包含更多维度的信息。枚举毕竟是对象,可以包含更多的信息。
- 枚举优点三:枚举可以直接用于Switch判断,代码通常会比常量+if/else简洁一些。
- 枚举做统一返回结果,可以有效规范错误码及错误信息。将错误码及错误信息实现绑定。
-
枚举策略:
-
根据会员返回打折力度。黄金会员:6折白银会员:7折;青铜会员:8折。
-
用枚举实现(在枚举类中定义抽象方法,强制子类实现):
- 这是基于,枚举中的每个具体元素,都是该枚举类引用指向的对象。
public class MemberDemo { public static void main(String[] args) { User user = new User(1L, "bravo", MemberEnum.GOLD_MEMBER.getType()); BigDecimal productPrice = new BigDecimal("1000"); BigDecimal discountedPrice = calculateFinalPrice(productPrice, user.getMemberType()); System.out.println(discountedPrice); } /** * 根据会员身份返回折扣后的商品价格 * * @param originPrice * @param user * @return */ public static BigDecimal calculateFinalPrice(BigDecimal originPrice, Integer type) { return MemberEnum.getEnumByType(type).calculateFinalPrice(originPrice); } } @Data @AllArgsConstructor class User { private Long id; private String name; /** * 会员身份 * 1:黄金会员,6折优惠 * 2:白银会员,7折优惠 * 3:青铜会员,8折优惠 */ private Integer memberType; } enum MemberEnum { // ---------- 把这几个枚举当做本应该在外面实现的MemberEnum之类,不要看成MemberEnum内部的 ---------- GOLD_MEMBER(1, "黄金会员") { @Override public BigDecimal calculateFinalPrice(BigDecimal originPrice) { return originPrice.multiply(new BigDecimal(("0.6"))); } }, SILVER_MEMBER(2, "白银会员") { @Override public BigDecimal calculateFinalPrice(BigDecimal originPrice) { return originPrice.multiply(new BigDecimal(("0.7"))); } }, BRONZE_MEMBER(3, "青铜会员") { @Override public BigDecimal calculateFinalPrice(BigDecimal originPrice) { return originPrice.multiply(new BigDecimal(("0.8"))); } }, ; // ---------- 下面才是MemberEnum类的定义 --------- private final Integer type; private final String desc; MemberEnum(Integer type, String desc) { this.type = type; this.desc = desc; } /** * 定义抽象方法,留个子类实现 * * @param originPrice * @return */ protected abstract BigDecimal calculateFinalPrice(BigDecimal originPrice); public Integer getType() { return type; } public String getDesc() { return desc; } public static MemberEnum getEnumByType(Integer type) { MemberEnum[] values = MemberEnum.values(); for (MemberEnum memberEnum : values) { if (memberEnum.getType().equals(type)) { return memberEnum; } } throw new IllegalArgumentException("Invalid Enum type:" + type); } }
-
iwehdio的博客园:https://www.cnblogs.com/iwehdio/