31.合理地结合泛型和可变参数
什么是可变参数
泛型都懂,但是什么是可变参数?可变参数是java1.5版本的新特性,顾名思义就是可以改变的参数,这里的改变是指参数个数可以改变,官方语言来说就是用户若是想定义一个方法,但是在此之前并不知道以后要用的时候想传几个参数进去,可以在方法的参数列表中写参数类型或者数组名,然后在方法内部直接用操作数组的方式操作
// 格式 数据类型...数组名
public void test(int… arr)
当调用一个可变参数时,会创建一个数组来保存可变参数,当可变参数具有泛化类型时,编译器会出现红色小灯泡,如下编译器提示去调泛型String
但是值得注意的虽然当明确创建一个泛型数组是非法的,但是声明一个带有泛型可变参数的方法是合法的
// Arrays.asList(T... a) Collections.addAll(Collection<? super T> c, T... elements)
public void argChange(T...lists ){
}
使用 @SafeVarargs 注解
在声明具有模糊类型(比如:泛型)的可变参数的构造函数或方法时,如图(下面这个代码可能将堆污染传播到调用栈)Java编译器会报unchecked警告。鉴于这些情况,如果程序员断定声明的构造函数和方法的主体不会对其varargs参数执行潜在的不安全的操作,可使用@SafeVarargs进行标记,这样的话,Java编译器就不会报unchecked警告
只能用于标记构造函数和
final
,static
方法,在 Java 9 中,它在私有实例方法中也变为合法,运行时候生效,不能被@SafeVarargs
标志的可以使用@SuppressWarnings("unchecked")
判断是否安全的方法
由于调用方法时会创建一个泛型数组,用来容纳可变参数,如果这个方法里面没有存储任何东西,并不对数组进行转义那么他是安全的,即可变参数数组仅用于从调用者向方法传递可变数量的参数
不合理使用例子
文中的例子代码如下
public class Test {
static <T> T[] toArray(T... args) {
return args;
}
static <T> T[] pickTwo(T a, T b, T c) {
switch (ThreadLocalRandom.current().nextInt(3)) {
case 0:
return toArray(a, b);
case 1:
return toArray(a, c);
case 2:
return toArray(b, c);
}
throw new AssertionError();
}
public static void main(String[] args) {
String[] pickTwo = pickTwo("Good", "Fast", "Cheap");
}
}
这个例子运行报错[Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
,不能将Object
转化为String
类型,编译此方法时,编译器会生成代码以创建一个将两个 T 实例传递给 toArray
的可变参数数组。 这段代码分配了一个 Object[]
类型的数组,它是保证保存这些实例的最具体的类型,而不管在调用位置传递给 pickTwo
的对象是什么类型。 toArray
方法只是简单地将这个数组返回给 pickTwo
,然后 pickTwo
将它返回给调用者,所以 pickTwo
总是返回一个 Object[]
类型的数组。在编译器的内部其实是偷偷的进行了一个Object与String数组的转换的,就是这里转换失败拉
安全使用例子
// 使用extends 确定了参数的范围
@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists)
result.addAll(list);
return result;
}
32. 优先考虑类型安全的异构容器
异构容器是指能够容纳不同类型对象的容器。像我们通常用的List
、Map
等容器,它们的原生态类型本身就是异构容器,一旦给它们设置了泛型参数,例如List<String>
、Map<Integer, String>
,它们就不再是异构容器。
使用Map实现下栗子
没有枚举之前
没有枚举之前是用int
,string
等声明的常量来代替的
缺点局限
- 没有提供类型安全的方式和任何表达力
- java必须为由
int
,string
等设置单独的名称前缀,防止命名冲突
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;。
- 如果和
int
,string
的值发生改变,需要重新编译客户端
使用枚举
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
public static void main(String[] args) {
Favorites f = new Favorites();
f.putFavorite(String.class, "Java");
f.putFavorite(Integer.class, 0xcafebabe);
f.putFavorite(Class.class, Favorites.class);
String favoriteString = f.getFavorite(String.class);
int favoriteInteger = f.getFavorite(Integer.class);
Class<?> favoriteClass = f.getFavorite(Class.class);
System.out.printf("%s %x %s%n", favoriteString, favoriteInteger, favoriteClass.getName());
}
}
内部用一个Map<Class<?>, Object>
来保存所有的爱好,使用Class<?>
作为键记录每个爱好的类型,而用Object
作为值不再区分它们的类型。当取出时,根据请求的类型从Map
中查找相应的值,由于值是Object
类型的,需要使用type.cast
强制转换为type
指定的类型。只要客户端按照API的要求使用,这里的强制转换一定不会出错。
局限性
这种实现方法有两种局限性
- 恶意客户端可以使用原始的
Class
对象,破坏Favorites实例的类型安全,解决办法是利用type.cast
进行动态转换如
public<T> void putFavorite(Class<T> type, T instance) {
favorites.put(type, type.cast(instance));
}
- 不能用于泛型化类型,例如,你无法把
List<String>
作为Favorites
的键,因为List<String>.class
是个语法错误。
33. 使用枚举类型替代整型常量
枚举的本质其实也是int值,它一定是实例可控的,因为客户端既然不能创建枚举类型的实例也不能继承它
使用方式
将数据与枚举常量相关联,声明实例属性并编写一个构造方法,构造方法带有数据并将数据保存在属性中。
public enum TextColor {
RED(1,"红色"),
GREEN(2,"绿色"),
YELLOW(3,"黄色");
private Integer code;
private String desc;
private TextColor(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
为特定于常量(constant-specific)的实现
// 在枚举类型中声明一个抽象的 apply方法,并用常量特定的类主体中的每个常量的具体方法重写它
PLUS("+"){
public double apply(double x, double y) {
return x + y;
}
},
MINUS("-") {
public double apply(double x, double y) {
return x - y;
}
},
TIMES("*") {
public double apply(double x, double y) {
return x * y;
}
},
DIVIDE("/"){
public double apply(double x, double y) {
return x / y;
}
};
private final String symbol;
private Operation(String symbol) {
this.symbol = symbol;
}
public String getSymbol() {
return symbol;
}
public abstract double apply(double x, double y);
34. 使用实例属性替代序数
ordinal
通常会返回枚举方法的序数,慎用
35. 使用 EnumSet 替代位属性
java.util
包提供了 EnumSet
类来有效地表示从单个枚举类型中提取的值集合,
EnumSet 是一个专为枚举设计的集合类,EnumSet中的所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建EnumSet时显式或隐式地指定
方法
EnumSet allOf(Class elementType)
: 创建一个包含指定枚举类里所有枚举值的EnumSet
集合。EnumSet complementOf(EnumSet e)
: 创建一个其元素类型与指定EnumSet
里元素类型相同的EnumSet
集合,新EnumSet
集合包含原EnumSet
集合所不包含的、此类枚举类剩下的枚举值(即新EnumSet
集合和原EnumSet
集合的集合元素加起来是该枚举类的所有枚举值)。EnumSet copyOf(Collection c)
: 使用一个普通集合来创建EnumSet集合。EnumSet copyOf(EnumSet e)
: 创建一个指定EnumSet具有相同元素类型、相同集合元素的EnumSet集合。EnumSet noneOf(Class elementType)
: 创建一个元素类型为指定枚举类型的空EnumSet。EnumSet of(E first,E…rest)
: 创建一个包含一个或多个枚举值的EnumSet集合,传入的多个枚举值必须属于同一个枚举类。EnumSet range(E from,E to)
: 创建一个包含从from枚举值到to枚举值范围内所有枚举值的EnumSet集合。
// 网上的例子https://blog.csdn.net/moakun/article/details/80617845
public class EnumSetTest {
public static void main(String[] args) {
//1.创建一个包含Session(枚举类)里所有枚举值的EnumSet集合
EnumSet e1 = EnumSet.allOf(Session.class);
System.out.println(e1);//[SPRING, SUMMER, FAIL, WINTER]
//2.创建一个空EnumSet
EnumSet e2 = EnumSet.noneOf(Session.class);
System.out.println(e2);//[]
//3. add()空EnumSet集合中添加枚举元素
e2.add(Session.SPRING);
e2.add(Session.SUMMER);
System.out.println(e2);//[SPRING, SUMMER]
//4. 以指定枚举值创建EnumSet集合
EnumSet e3 = EnumSet.of(Session.SPRING,Session.FAIL);
System.out.println(e3);//[SPRING, FAIL]
//5.创建一个包含从from枚举值到to枚举值范围内所有枚举值的EnumSet集合。
EnumSet e4 = EnumSet.range(Session.SPRING,Session.FAIL);
System.out.println(e4);//[SPRING, SUMMER, FAIL]
//6.创建一个其元素类型与指定EnumSet里元素类型相同的EnumSet集合,
// 新EnumSet集合包含原EnumSet集合所不包含的枚举值
EnumSet e5 = EnumSet.complementOf(e4);
System.out.println(e5);//[WINTER]
}
}
//创建一个枚举
enum Session{
SPRING,
SUMMER,
FAIL,
WINTER
}
36. 使用 EnumMap 替代序数索引
EnumMap
是Map
接口的实现,其key-value
映射中的key
是Enum
类型,具体用法网上有很多,可以自行百度,哈哈哈哈
37. 使用接口模拟可扩展的枚举
这里就是那么一说有这么一种用法,按照上面那个操作码的枚举例子,改为接口模拟代码如下
// 这里虽然枚举类型( BasicOperation )不可扩展,但接口类型( Operation )是可扩展的,曲线救国
public interface Operation {
double apply(double x, double y);
}
public enum BasicOperation implements Operation {
PLUS("+") {
public double apply(double x, double y) {
return x + y;
}
},
MINUS("-") {
public double apply(double x, double y) {
return x - y;
}
},
TIMES("*") {
public double apply(double x, double y) {
return x * y;
}
},
DIVIDE("/") {
public double apply(double x, double y) {
return x / y;
}
};
private final String symbol;
BasicOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
}
38. 注解优于命名模式
过去,通常使用命名模式(naming patterns)来指示某些程序元素需要通过工具或框架进行特殊处理。例如JUnit
以前是通过test开始名称来指定测试方法这种方式虽然有效,但是也存在缺点:
- 拼写错误导致失败
- 是无法确保它们仅用于适当的程序元素
- 是它们没有提供将参数值与程序元素相关联的好的方法
后来出现了@Test
解决了这一系列的问题
39. 始终使用 Override 注解
这个很简单,子类必须标记@Override
注解
40. 使用标记接口定义类型
标记接口(marker interface),不包含方法声明,只是指定(或“标记”)一个类实现了具有某些属性的接口。例如家喻户晓的Serializable
接口,通过实现这个接口,一个类表明他的实例可以被序列化或者反序列化
标记接口定义了一个由标记类实例实现的类型;标记注解则不会。 标记接口类型的存在允许在编译时捕 获错误,如果使用标记注解,则直到运行时才能捕获错误。
标记接口对于标记注解的另一个优点是可以更精确地定位目标。 如果使用目标 ElementType.TYPE
声明注解
类型,它可以应用于任何类或接口。 假设有一个标记仅适用于特定接口的实现。 如果将其定义为标记接口,则可以
扩展它适用的唯一接口,保证所有标记类型也是适用的唯一接口的子类型