Java编译时允许同一个文件内定义多个顶级的类,但是这么做存在很大的风险。例如以下代码:
public class Main {public static void main(String[] args) {System.out.println(Utensil.NAME + Dessert.NAME);}
}
打印出两个类的NAME属性的拼接结果。你可以在某一个文件中直接定义这两个类:
// Utensil.java
class Utensil {static final String NAME = "pan";
}
class Dessert {static final String NAME = "cake";
}
如果编译的时候会输出:“pancake”。我们还可以在另一个文件中继续定义这两个类:
// Dessert.java
class Utensil {static final String NAME = "pot";
}
class Dessert {static final String NAME = "pie";
}
此时执行javac Main.java Dessert.java会提示编译报错,因为Utensil和Dessert被两个文件重复定义;如果执行javac Main.javaor javac Main.java Utensil.java,将会输出“pancake”;如果执行javac Dessert.java Main.java,会输出“potpie”,这种会根据编译顺序而改变输出结果的现象是不可接受的。
所以为了解决这个问题,应该把顶级的类分开定义到不同的问题。如果仍然试图把多个顶级类定义到同一个文件,应该考虑使用静态成员变量:
// Static member classes instead of multiple top-level classes
public class Test {public static void main(String[] args) {System.out.println(Utensil.NAME + Dessert.NAME);}private static class Utensil {static final String NAME = "pan";}private static class Dessert {static final String NAME = "cake";}
}
在使用带有泛型的工具类时,不要使用未经定义的类型,例如:
// 指定泛型
List list = new List<>();
// 未指定泛型
List list2 = new List();
两种的区别在于:指定了泛型的工具类会在编译时检查添加的元素是否符合泛型的要求,防止添加泛型以外的元素导致运行时异常。
案例一:
public static void main(String[] args) {List strings = new ArrayList<>();// 此处添加了Integer类型unsafeAdd(strings, Integer.valueOf(42));// 运行时报错,Integer不能转化String,ClassCastExceptionString s = strings.get(0); // Has compiler-generated cast
}
// List没有定义泛型,编译时检测不出在List添加了Integer
private static void unsafeAdd(List list, Object o) {list.add(o);
}
List只允许添加String类型,但是因为unsafeAdd的List使用了未经定义的泛型,导致编译时没有发现添加的元素不符合泛型要求。
案例二:
static int numElementsInCommon(Set s1, Set s2) {int result = 0;for (Object o1 : s1)if (s2.contains(o1))result++;return result;
}
numElementsInCommon方法参数Set没有设置泛型,编译时有警告但是运行时不会有异常,但是这么做是有风险的,比如可以随意的添加元素导致之后出现ClassCastException。
// 添加泛型通配符 ?
static int numElementsInCommon(Set> s1, Set> s2) {int result = 0;for (Object o1 : s1)if (s2.contains(o1))result++;return result;
}
通配符?表示任意类型的元素,但是只允许获取元素不能添加元素,添加元素会编译报错。
WildCard.java:13: error: incompatible types: String cannot be
converted to CAP#1
c.add(“verboten”);
^
where CAP#1 is a fresh type-variable:
CAP#1 extends Object from capture of ?
使用instanceof判断Set时,应当强转成Set>,而不是Set。
总结:以Set为例,应该设置泛型而不是直接使用Set,任何类型的原色可以考虑使用Set或Set>。
当使用泛型的时候,将会看到各种各样的警告,应该尽可能的解决这些警告。
有些警告比较简单,目前Android studio点击黄色警告会有快速解决提示,比如:
// 修改前,提示warning: [unchecked] unchecked conversion
Set exaltation = new HashSet();
// 修改后,警告消失
Set exaltation = new HashSet<>();
还有一些警告解决起来比较复杂,可以考虑instanceof等操作符减少ClassCastException。如果无法消除警告但是可以确保该代码是安全的,可以使用@SuppressWarnings(“unchecked”)注解,在编译时忽略此次警告,要注意@SuppressWarnings使用的范围要尽可能的小,否则会导致范围内的代码的其他警告也会被忽略。以下为案例:
// 该方法是把ArrayList转换成Array
public T[] toArray(T[] a) {if (a.length < size)// 此处会有警告:warning: [unchecked] unchecked cast// 但是通过分析,所有操作的类型都是泛型T,可以确保不会有异常发生return (T[]) Arrays.copyOf(elements, size, a.getClass());System.arraycopy(elements, 0, a, 0, size);if (a.length > size)a[size] = null;return a;
}// 添加SuppressWarnings消除警告
@SuppressWarnings
public T[] toArray(T[] a) {if (a.length < size) {// This cast is correct because the array we're creating// is of the same type as the one passed in, which isT[].@SuppressWarnings("unchecked") T[] result =(T[]) Arrays.copyOf(elements, size, a.getClass());return result;}System.arraycopy(elements, 0, a, 0, size);if (a.length > size)a[size] = null;return a;
}
在使用@SuppressWarnings应当用注释写明忽略警告的原因,有助于日后维护代码。
Sub是Super的子类:Sub[]是Super[]的子集合,但是List不是List
的子集合。
// 因为Long[]是Object[]的子集合,所以编译不会报错
Object[] objectArray = new Long[1];
// 运行时抛出异常:ArrayStoreException
objectArray[0] = "I don't fit in"; // List不是List
代码示例一:
List[] stringLists = new List[1]; // (1)
List intList = List.of(42);
Object[] objects = stringLists;
objects[0] = intList;
String s = stringLists[0].get(0);
第一句:创建List数组,因为Array需要明确知道类型,但是List使用的泛型,导致Array不能明确知道内部元素的类型,所以语法会报错;
如果第一句不报错,那么就会执行接下来的代码,导致在List
代码实例二:
public class Chooser {private final Object[] choiceArray;public Chooser(Collection choices) {choiceArray = choices.toArray();}public Object choose() {Random rnd = ThreadLocalRandom.current();return choiceArray[rnd.nextInt(choiceArray.length)];}
}
因为Array不支持泛型,所以返回元素的类型为Object。可以通过添加泛型修改代码:
public class Chooser {private final T[] choiceArray;public Chooser(Collection choices) {// 此处报错:Object[] cannot be converted to T[]choiceArray = choices.toArray();}
}
因为choices.toArray()返回的是Object[],所以此处会报错,但是我们知道choices.toArray()返回的数组的元素一定是
类型,所以可以安全的增加强制类型转换:
public class Chooser {private final T[] choiceArray;public Chooser(Collection choices) {// 此处会有警告:warning: [unchecked] unchecked castchoiceArray = (T[])choices.toArray();}
}
因为我们可以确保此处不会出现类型转换异常,可以使用@SuppressWarnings消除警告。如果使用List则不会出现语法错误和警告:
public class Chooser {private final List choiceList;public Chooser(Collection choices) {choiceList = new ArrayList<>(choices);}public T choose() {Random rnd = ThreadLocalRandom.current();return choiceList.get(rnd.nextInt(choiceList.size()));}
}
虽然使用List会有一些代码冗余和运行效率会有一点下降,但是不需要做类型检查就可以保证不会出现ClassCastException,所以使用List的实现效果比Array更好;
泛型可以让定义的类更具有通用性,且不需要额外的类型转换,应该多考虑使用泛型。以下为案例:
public class Stack {private Object[] elements;private int size = 0;private static final int DEFAULT_INITIAL_CAPACITY = 16;public Stack() {elements = new Object[DEFAULT_INITIAL_CAPACITY];}public void push(Object e) {ensureCapacity();elements[size++] = e;}public Object pop() {if (size == 0)throw new EmptyStackException();Object result = elements[--size];elements[size] = null; // Eliminate obsolete referencereturn result;}public boolean isEmpty() {return size == 0;}private void ensureCapacity() {if (elements.length == size)elements = Arrays.copyOf(elements, 2 * size + 1);}
}
Stack类的元素类型为Object,使用Stack.pop还需要自己类型转换,这种情况非常适合使用泛型,只需把Stack类添加泛型
E后,把所有的Object改为E即可:
public class Stack {private E[] elements;private int size = 0;private static final int DEFAULT_INITIAL_CAPACITY = 16;public Stack() {elements = new E[DEFAULT_INITIAL_CAPACITY];}public void push(E e) {ensureCapacity();elements[size++] = e;}public E pop() {if (size == 0)throw new EmptyStackException();E result = elements[--size];elements[size] = null; // Eliminate obsolete referencereturn result;}... // no changes in isEmpty or ensureCapacity
}
之后会出现一些类型检查的警告或报错,例如:
public Stack() {elements = new E[DEFAULT_INITIAL_CAPACITY];
}
因为Array需要具体的类型,所以此处会报错不能创建E类型的Array,可以通过强制类型转换解决:
public Stack() {elements = (E[])new Object[DEFAULT_INITIAL_CAPACITY];
}
此时会出现类型未检查的警告,因为elements是私有的,且只有push方法可以添加E类型元素,所以可以确保是安全的,通过注解消除警告:
// The elements array will contain only E instances from
push(E).
// This is sufficient to ensure type safety, but the runtime
// type of the array won't be E[]; it will always be Object[]!
@SuppressWarnings("unchecked")
public Stack() {elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
吃了修改elements,还可以保留elements的Object类型,修改pop方法的返回值类型为E:
public E pop() {if (size == 0)throw new EmptyStackException();// push requires elements to be of type E, so cast is@SuppressWarnings("unchecked") E result = (E) elements[--size];elements[size] = null; // Eliminate obsolete referencereturn result;
}
修改完成后,Stack可以使用任意类型,且不需要类型强制转换就可以直接使用元素的类型:
// Little program to exercise our generic Stack
public static void main(String[] args) {Stack stack = new Stack<>();for (String arg : args)stack.push(arg);while (!stack.isEmpty())System.out.println(stack.pop().toUpperCase());
}
union方法是用来合并两个集合:
public static Set union(Set s1, Set s2) {Set result = new HashSet(s1);result.addAll(s2);return result;
}
这种写法会出现警告:
Union.java:5: warning: [unchecked] unchecked call to
HashSet(Collection extends E>) as a member of raw type
HashSet
Set result = new HashSet(s1);
^
Union.java:6: warning: [unchecked] unchecked call to
addAll(Collection extends E>) as a member of raw type Set
result.addAll(s2);
为了解决这些警告并且让程序更安全,应该确保set1和set2中的类型相同,并且返回值的类型也应该保持一致,通过设置参数的泛型和返回值的泛型实现:
public static Set union(Set s1, Set s2) {Set result = new HashSet<>(s1);result.addAll(s2);return result;
}
这种实现方式通常用来实现一些通用的工具方法,例如Collections中的binarySearch,reverse等等。
另外一种常用的用法是通过自定义接口设置泛型,对特定类型定制操作,实现类似函数作为方法参数的效果。例如常见的:
public interface Comparable {int compareTo(T o);
}default void sort(Comparator super E> c) {Object[] a = this.toArray();Arrays.sort(a, (Comparator) c);ListIterator i = this.listIterator();for (Object e : a) {i.next();i.set((E) e);}}
这种实现更加灵活,例如一个包含了学生信息的List,既可以根据学号排序又可以根据名称排序。
所以在定义方法时应当优先考虑使用泛型方法,减少类似方法重复定义的情况。