您的位置:首页 > Web前端

《Effective Java》——学习笔记(泛型&枚举和注解)

2018-02-28 16:46 387 查看

泛型

第23条:请不要在新代码中使用原生态类型

如果使用原生态类型,就失掉了泛型在安全性和表述性方面的所有优势,比如不应该使用像List这样的原生态类型,而应该使用如List<Object>、List<String>这样的类型

第24条:消除非受检警告

用泛型编程时,会遇到许多编译器警告:非受检强制转化警告、非受检方法调用警告、非受检普通数组创建警告,以及非受检转换警告。如:

Set<Lark> exaltation = new HashSet();

=>

Set<Lark> exaltation = new HashSet<Lark>();


要尽可能地消除每一个非受检警告,如果消除了所有警告,就可以确保代码是类型安全的,意味着不会在运行时出现ClassCastException异常

如果无法消除警告,同时可以证明引起警告的代码是类型安全,(只有在这种情况下才)可以用一个@SuppressWarnings(“unchecked”)注解来禁止这条警告

SuppressWarnings注解可以用在任何粒度的级别中,从单独的局部变量声明到整个类都可以,应该始终在尽可能小的范围中使用SuppressWarnings注解。永远不要在整个类上使用SuppressWarnings,这么做可能会掩盖了重要的警告

每当使用@SuppressWarnings(“unchecked”)注解时,都要添加一条注释,说明为什么这么做是安全的

第25条:列表优先于数组

数组与泛型相比,有两个重要的不同点。首先,数组是协变的,泛型是不可变的,如下例:

这段代码是合法的

// Fails at runtime!
Object[] objectArray = new Long[1];
objectArray[0] = "I donot fit in";


但下面这段代码则不合法

// Wonot compile!
List<Object> o1 = new ArrayList<Long>();
o1.add("I donot fit in");


数组与泛型之间的第二大区别在于,数组是具体化的,因此数组会在运行时才知道并检查它们的元素类型约束。相比之下,泛型则是通过擦除来实现的,因此泛型只在编译时强化它们的类型信息,并在运行时丢弃(或者擦除)它们的元素类型信息

第26条:优先考虑泛型

使用泛型比使用需要在客户端中进行转换的类型来得更加安全,也更加容易,在设计新类型的时候,要确保它们不需要这种转换就可以使用,这通常意味着要把类做成是泛型的,只要时间允许,就把现有的类型都泛型化。这对于这些类型的新用户来说会变得更加轻松,又不会破坏现有的客户端

第27条:优先考虑泛型方法

编写泛型方法与编写泛型类型相类似,声明类型参数的类型参数列表,处在方法的修饰符及其返回类型之间,如下例,类型参数列表为<E>,返回类型为Set<E>

public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}


泛型方法的一个显著特性是,无需明确指定类型参数的值,编译器通过检查方法参数的类型来计算类型参数的值

第28条:利用有限制通配符来提升API的灵活性

Java提供了一种特殊的参数化类型,称作有限制的通配符类型,<? Extends E>称为“E的某个子类型”,<? super E>称为“E的某种超类”

为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型,如果参数化类型表示一个T生成者,就使用<? extends T>;如果它表示一个T消费者,就使用<? super T>,可以通过PECS记忆,producer-extends,consumer-super,如下例:

List<Apple> apples = new ArrayList<Apple>();
List<? extends Fruit> fruits = apples;
// 此时只能从fruits中取出数据,而不能再add数据,因为fruits中的数据类型是Fruit的子类,但是具体是什么类型并不知道,所以fruits.add(...);操作是非法的,而Fruit fruit = fruits.get(0);是合法的
// <? extends T> 只能取(产生)数据,所以是生成者(producer)

List<Fruit> fruits = new ArrayList<Fruit>();
List<? super Apple> = fruits;
// 此时fruits中的数据类型是Apple的超类,但是具体是什么类型并不知道,所以取出的数据类型只能是Object类型,而fruits.add(new Apple());fruits.add(new GreenApple());操作是合法的

// <? super T>只能存(消费)数据,所以是消费者(consumer)


JDK 8 中的 Collections.copy()源码如下:

/**
* Copies all of the elements from one list into another.  After the
* operation, the index of each copied element in the destination list
* will be identical to its index in the source list.  The destination
* list must be at least as long as the source list.  If it is longer, the
* remaining elements in the destination list are unaffected. <p>
*
* This method runs in linear time.
*
* @param  <T> the class of the objects in the lists
* @param  dest The destination list.
* @param  src The source list.
* @throws IndexOutOfBoundsException if the destination list is too small
*         to contain the entire source List.
* @throws UnsupportedOperationException if the destination list's
*         list-iterator does not support the <tt>set</tt> operation.
*/
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");

if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}


一般来说,如果类型参数只在方法声明中出现一次,就可以用通配符取代它,如果是无限制的类型参数,就用无限制的通配符取代它;如果是有限制的类型参数,就用有限制的通配符取代它

第29条:优先考虑类型安全的异构容器

将键(key)进行参数化而不是将容器参数化,然后将参数化的键提交给容器,来插入或者获取值,用泛型系统来确保值的类型与它的键相符

如下例:

public class Favorites {
private Map<Class<?>, Object> favorites =
new HashMap<Class<?>, Object>();

public <T> void putFavorite(Class<T> type, T instance) {
if(type == null) {
throw new NullPointerException("Type is null");
}
favorites.put(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(Class.class, Favorites.class);
String favoriteStr = f.getFavorite(String.class);
Class favoriteClass = f.getFavorite(Class.class);
}


Favorites实例是异构的:不像普通的map,它的所有键都是不同类型的,因此,称Favorites为类型安全的异构容器

枚举和注解

第30条:用enum代替int常量

枚举类型是指由一组固定的常量组成合法值的类型,在编程语言还没有引入枚举类型之前,表示枚举类型的常用模式是声明一组具名的int常量,每个类型成员一个常量:

public static final int APPLE_FUJI = 0;


这种方法称作int枚举模式,存在着诸多不足,因为int枚举是编译时常量,被编译到使用它们的客户端中,如果与枚举常量关联的int发生了变化,客户端就必须重新编译。如果没有重新编译,程序还是可以运行,但是它们的行为就是不确定

public static final String MQ_TOPIC_VQ_AUDIO_PROCESS = "TOPIC_VQ_AUDIO_PROCESS";


上面这种模式叫做String枚举模式,会有性能问题,因为它依赖于字符串的比较操作,还会导致初级用户把字符串常量硬编码到客户端代码中,而不是使用恰当的域名

从Java1.5开始,提供了另一种可以替代的解决方法,可以避免int和String枚举模式的缺点,并提供许多额外的好处

public enum Apple { FUJI, PRIPPIN, GRANNY_SMITH };


Java的枚举本质是int值,就是通过公有的静态final域为每个枚举常量导出实例的类,因为没有可以访问的构造器,枚举类型是真正的final

枚举提供了编译时的类型安全,如果声明一个参数的类型为Apple,就可以保证,被传到该参数上的任何非null的对象引用一定属于三个有效的Apple值之一。试图传递类型错误的值时,会导致编译时错误

包含同名常量的多个枚举类型可以在一个系统中和平共处,因为每个类型都有自己的命名空间。可以增加或者重新排列枚举类型中的常量,而无需重新编译它的客户端代码,因为导出常量的域在枚举类型和它的客户端之间提供了一个隔离层:常量值并没有被编译到客户端代码中,而是在int枚举模式之中

枚举类型还允许添加任意的方法和域,并实现任意的接口,它们提供了所有Object方法的高级实现,实现了Comparable和Serializable接口,并针对枚举类型的可任意改变性设计了序列化方式

示例如下:

// Enum type with data and behavior
public enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS  (4.869e+24, 6.052e6),
EARTH  (5.975e+24, 6.378e6),
MARS   (6.419e+23, 3.393e6),
JUPITER(1.899e+27, 7.149e7),
SATURN (5.685e+26, 6.027e7),
URANUS (8.683e+25, 2.556e7),
NEPTUNE(1.024e+26, 2.477e7);
private final double mass;           // In kilograms
private final double radius;         // In meters
private final double surfaceGravity; // In m / s^2

// Universal gravitational constant in m^3 / kg s^2
private static final double G = 6.67300E-11;

// Constructor
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
surfaceGravity = G * mass / (radius * radius);
}

public double mass()           { return mass; }
public double radius()         { return radius; }
public double surfaceGravity() { return surfaceGravity; }

public double surfaceWeight(double mass) {
return mass * surfaceGravity;  // F = ma
}
}


编写一个像Planet这样的枚举类型并不难,为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器

如果一个枚举具有普遍适用性,它就应该成为一个顶层类,如果它只是被用在一个特定的顶层类,它就应该成为该顶层类的一个成员类

将不同的行为与每个枚举常量关联起来,可以在枚举类型中声明一个抽象的apply方法,并在特定于常量的类主体中,用具体的方法覆盖每个常量的抽象apply方法,这种方法被称作特定于常量的方法实现

public enum Operation {
PLUS("+") {
double apply(double x, double y) { return x + y; }
},
MINUS("-") {
double apply(double x, double y) { return x - y; }
},
TIMES("*") {
double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
double apply(double x, double y) { return x / y; }
};
private final String symbol;
Operation(String symbol) { this.symbol = symbol; }
@Override public String toString() { return symbol; }

abstract double apply(double x, double y);

// Implementing a fromString method on an enum type
private static final Map<String, Operation> stringToEnum
= new HashMap<String, Operation>();
static { // Initialize map from constant name to enum constant
for (Operation op : values())
stringToEnum.put(op.toString(), op);
}
// Returns Operation for string, or null if string is invalid
public static Operation fromString(String symbol) {
return stringToEnum.get(symbol);
}

// Test program to perform all operations on given operands
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
for (Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}
}


枚举类型有一个自动产生的valueOf(String)方法,它将常量的名字转变成常量本身,如果在枚举类型中覆盖toString,要考虑编写一个fromString方法,将定制的字符串表示法变回相应的枚举

总之,与int常量相比,枚举要易读得多,也更加安全,功能更加强大

第31条:用实例域代替序数

永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中:

// Enum with integer data stored in an instance field
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
NONET(9), DECTET(10), TRIPLE_QUARTET(12);

private final int numberOfMusicians;
Ensemble(int size) { this.numberOfMusicians = size; }
public int numberOfMusicians() { return numberOfMusicians; }
}


第32条:用EnumSet代替位域

java.util包提供了EnumSet类来有效地表示从单个枚举类型中提取的多个值的多个集合,这个类实现Set接口,提供了丰富的功能、类型安全性,以及可以从任何其他Set实现中得到的互用性

public class Text {
public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }

// Any Set could be passed in, but EnumSet is clearly best
public void applyStyles(Set<Style> styles) {
// Body goes here
}

// Sample use
public static void main(String[] args) {
Text text = new Text();
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
}
}


第33条:用EnumMap代替序数索引

有一种非常快速的Map实现专门用于枚举键,称作java.util.EnumMap

public class Herb{
public enum Type {ANNUAL, PERENNIAL, BIENNIAL};

private final String name;
private final Type type;

Herb(String name, Type type){
this.name = name;
this.type = type;
}

@Override
public String toString(){
return name;
}

public static void main(String[] args) {
Herb[] garden = {
new Herb("Basil",    Type.ANNUAL),
new Herb("Carroway", Type.BIENNIAL),
new Herb("Dill",     Type.ANNUAL),
new Herb("Lavendar", Type.PERENNIAL),
new Herb("Parsley",  Type.BIENNIAL),
new Herb("Rosemary", Type.PERENNIAL)
};

// Using an EnumMap to associate data with an enum
Map<Herb.Type, Set<Herb>> herbsByType =
new EnumMap<Herb.Type, Set<Herb>>(Herb.Type.class);
for (Herb.Type t : Herb.Type.values())
herbsByType.put(t, new HashSet<Herb>());
for (Herb h : garden)
herbsByType.get(h.type).add(h);
System.out.println(herbsByType);
}
}


第34条:用接口模拟可伸缩的枚举

虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,对它进行模拟。这样允许客户端编写自己的枚举来实现接口,如果API是根据接口编写的,那么在可以使用基础枚举类型的任何地方,也都可以使用这些枚举

// Emulated extensible enum using an interface
public interface Operation {
double apply(double x, double y);
}

// Emulated extensible enum using an interface - Basic implementation
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;
}
}

// Emulated extension enum
public enum ExtendedOperation implements Operation {
EXP("^") {
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
REMAINDER("%") {
public double apply(double x, double y) {
return x % y;
}
};

private final String symbol;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@Override public String toString() {
return symbol;
}

// Test class to exercise all operations in "extension enum"
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(ExtendedOperation.class, x, y);

System.out.println();  // Print a blank line between tests
test2(Arrays.asList(ExtendedOperation.values()), x, y);
}

// test parameter is a bounded type token  (Item 29)
private static <T extends Enum<T> & Operation> void test(
Class<T> opSet, double x, double y) {
for (Operation op : opSet.getEnumConstants())
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}

// test parameter is a bounded wildcard type (Item 28)
private static void test2(Collection<? extends Operation> opSet,
double x, double y) {
for (Operation op : opSet)
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}
}


第35条:注解优先于命名模式

定义一个注解类型来指定简单的测试,它们自动运行,并在抛出异常时失败

// Marker annotation type declaration
import java.lang.annotation.*;

/**
* Indicates that the annotated method is a test method.
* Use only on parameterless static methods.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}


Test注解类型的声明就是它自身通过Retention和Target注解进行了注解,注解类型声明中的这种注解被称作元注解。@Retention(RetentionPolicy.RUNTIME)元注解表明,Test注解应该在运行时保留,如果没有保留,测试工具就无法知道Test注解。@Target(ElementType.METHOD)元注解表明,Test注解只在方法声明中才是合法的:它不能运用到类声明、域声明或者其他程序元素上

下面是对Test注解的应用,称作标记注解,因为它没有参数,只是“标注”被注解的元素,如果拼错Test,或者将Test注解应用到程序元素而非方法声明,程序就无法编译:

// Program containing marker annotations
public class Sample {
@Test public static void m1() { }  // Test should pass
public static void m2() { }
@Test public static void m3() {    // Test Should fail
throw new RuntimeException("Boom");
}
public static void m4() { }
@Test public void m5() { } // INVALID USE: nonstatic method
public static void m6() { }
@Test public static void m7() {    // Test should fail
throw new RuntimeException("Crash");
}
public static void m8() { }
}


如上8个方法,只有m1测试会通过,m3和m7会抛出异常,m5是一个实例方法,不属于注解的有效使用

注解永远不会改变被注解代码的语义,但是使它可以通过工具进行特殊的处理

public class RunTests {
public statis void main(String[] args) throw Exception {
int tests = 0;
int passed = 0;
Class testClass = Class.forName(args[0]);
for(Method m : testClass.getDeclaredMethods()){
if(m.isAnnotationPresent(Test.class)){
tests ++;
try {
m.invoke(null);
passed ++;
} catch(InvocationTargetException wrappedExc){
Throwable exc = wrappedExc.getCause();
System.out.println(m + " failed: " + exc);
} catch(Exception exc) {
System.out.println("INVALID @Test: " + m);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
}
}


RunTests通过调用Method.invoke反射式地运行类中所有标注了Test的方法,isAnnotationPresent告知该工具要运行哪些方法。如果测试方法抛出异常,反射机制就会将它封装在InvocationTargetException中

如果要针对只在抛出特殊异常时才成功的测试添加支持,需要一个新的注解类型:

// Annotation type with an array parameter

import java.lang.annotation.*;

/**
* Indicates that the annotated method is a test method that
* must throw the any of the designated exceptions to succeed.
*/

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception>[] value();
}


使用此注解

// Code containing an annotation with an array parameter
@ExceptionTest({ IndexOutOfBoundsException.class,
NullPointerException.class })
public static void doublyBad() {
List<String> list = new ArrayList<String>();

// The spec permits this method to throw either
// IndexOutOfBoundsException or NullPointerException
list.addAll(5, null);
}


修改测试工具来处理新的Exception

// Array ExceptionTest processing code
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (Throwable wrappedExc) {
Throwable exc = wrappedExc.getCause();
Class<? extends Exception>[] excTypes =
m.getAnnotation(ExceptionTest.class).value();
int oldPassed = passed;
for (Class<? extends Exception> excType : excTypes) {
if (excType.isInstance(exc)) {
passed++;
break;
}
}
if (passed == oldPassed)
System.out.printf("Test %s failed: %s %n", m, exc);
}
}


所有的程序员都应该使用Java平台所提供的预定义的注解类型,还要考虑使用IDE或者静态分析工具所提供的任何注解,这种注解可以提升由这些工具所提供的诊断信息的质量

第36条:坚持使用Override注解

坚持使用这个注解,可以防止一大类的非法错误

IDE具有自动检查功能,称作代码检验,如果启动相应的代码检验功能,当有一个方法没有Override注解,却覆盖了超类方法时,IDE就会产生一条警告

第37条:用标记接口定义类型

标记接口是没有包含方法声明的接口,而只是指明(或者标明)一个类实现了具有某种属性的接口,如Serializable接口,通过实现这个接口,类表明它的实例可以被写到ObjectOutputStream(被序列化)

标记接口胜过标记注解的一个优点是,标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型,这个类型允许在编译时捕捉在使用标记注解的情况下要到运行时才能捕捉到的错误

标记接口胜过标记注解的另一个优点是,它们可以被更加精确地进行锁定,如果注解类型利用@Target(ElementType.TYPE)声明,它就可以被应用到任何类或者接口。假设有一个标记只适用于特殊接口的实现,如果将它定义成一个标记接口,就可以用它将唯一的接口扩展成它适用的接口,如Set接口就是有限制的接口,这种标记接口可以描述整个对象的某个约束条件,或者表明实例能够利用其他某个类的方法进行处理

标记注解胜过标记接口的优点在于,它可以通过默认的方式添加一个或者多个注解类型元素,给已被使用的注解类型添加更多的信息

标记注解的另一个优点在于,它们是更大的注解机制的一部分

如果标记是应用到任何程序元素而不是类或者接口,就必须使用注解;如果标记只应用给类和接口,就应该优先使用标记接口
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: