《Effective.Java》 阅码草堂笔记 四

1,554 阅读9分钟

上一篇:《Effective.Java》 阅码草堂笔记 三

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. 优先考虑类型安全的异构容器

异构容器是指能够容纳不同类型对象的容器。像我们通常用的ListMap等容器,它们的原生态类型本身就是异构容器,一旦给它们设置了泛型参数,例如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 替代序数索引

EnumMapMap接口的实现,其key-value映射中的keyEnum类型,具体用法网上有很多,可以自行百度,哈哈哈哈

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 声明注解 类型,它可以应用于任何类或接口。 假设有一个标记仅适用于特定接口的实现。 如果将其定义为标记接口,则可以 扩展它适用的唯一接口,保证所有标记类型也是适用的唯一接口的子类型

下一篇:《Effective.Java》 阅码草堂笔记 五