java:effectivejava学习笔记之优先考虑泛型和泛型方法(代码片段)

JMW1407 JMW1407     2023-03-05     544

关键词:

Java优先考虑泛型和泛型方法

1、优先考虑泛型

下面我们举个例子,将他作为泛型化的主要备选对象,换句话说,可以适当的强化这个类来利用泛型。

public class Stack 

    private Object[] element;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 1;

    public Stack() 
        element = new Object[DEFAULT_INITIAL_CAPACITY];
    

    public void push(Object e) 
        ensureCapacity();
        element[size++] = e;
    

    public Object pop() 
        if (size == 0) 
            throw new EmptyStackException();
        
        Object result = element[--size];
        element[size] = null;
        return result;
    

    public boolean isEmpty() 
        return size == 0;
    

    private void ensureCapacity() 
        if (element.length == size) 
            element = Arrays.copyOf(element, 2 * size + 1);
        
    


首先我们代码进行简单的转换,先用类型参数替换所有的Object类型:

public class StackImprove1<E> 
    
    private E[] element;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 1;

    public StackImprove() 
    	//这里会爆编译错误
        element=new E[DEFAULT_INITIAL_CAPACITY];
    

    public void push(E e) 
        ensureCapacity();
        element[size++] = e;
    

    public E pop() 
        if (size == 0) 
            throw new EmptyStackException();
        
        E result = element[--size];
        element[size] = null;
        return result;
    

    public boolean isEmpty() 
        return size == 0;
    

    private void ensureCapacity() 
        if (element.length == size) 
            element = Arrays.copyOf(element, 2 * size + 1);
        
    
    


通常,你将至少得到一个错误或者警告,这个类也不例外。幸运的是,这个类只产生一个错误,如下:

    Stack.java:8: generic array creation
        elements = new E[DEFAULT_INITIAL_CAPACITY];

你不能创建不可具体化的(non-reifiable)类型的数组,如E。每当编写用数组支持的泛型时,都会出现这个问题。解决这个问题有两种方法。

第一种,直接绕过创建泛型数组的禁令:创建一个Object的数组,并将它转换成泛型数组类型。现在错误是消除了,但是编译器会产生一条警告。这种用法是合法的,但(整体上而言)不是类型安全的:

Stack.java:8: warning: [unchecked] unchecked cast 
found: Object[], required: E[]
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
                  ^

编译器不可能证明你的程序是类型安全的,但是你可以证明。你自己必须确保未受检的转换不会危及到程序的类型安全性。问题中的数组(elements)被保存在一个私有的域中,永远不会被返回到客户端,或者传给任何其他方法。

  • 这个数组中保存的唯一元素,是传给push方法的那些元素,它们的类型为E,因此未受检的转换不会有任何危害。

一旦你证明了未受检的转换是安全的,就要在尽可能小的范围中禁止警告(第27项)。在这种情况下,构造器只包含未受检的数组创建,因此可以在整个构造器中禁止这条警告。通过增加一条注解来完成禁止,Stack能够正确无误地进行编译,你就可以使用它了,无需显示的转换,也无需担心会出现ClassCastException异常:

// 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];


2)第二种方法:将elements域的类型从E[ ]改为Object[ ],之后把数组中获得的元素由Object转换为E

Stack.java:19: warning: [unchecked] unchecked cast
found: Object, required: E
    E result = (E) elements[--size];                         ^
  • 由于E是一个不可具体化的类型,编译器无法在运行时检验转换。你还是可以自己证实未受检的转换是安全的,因此可以禁止该警告。
  • 我们只要在包含未受检转换的赋值上禁止警告,而不是在整个pop方法上就可以了,如下:

下面我们对上面的代码进行第二种方式的改造:

// Appropriate suppression of unchecked warning
public E pop() 
    if (size == 0)
        throw new EmptyStackException();
    // push requires elements to be of type E, so cast is correct
    @SuppressWarnings("unchecked") 
    E result = (E) elements[--size];
    elements[size] = null; // Eliminate obsolete reference
    return result;


全部代码

public class StackImprove2<E> 

    private Object[] element;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 1;

    public StackImprove2() 
        element = new Object[DEFAULT_INITIAL_CAPACITY];
    

    public void push(E e) 
        ensureCapacity();
        element[size++] = e;
    

    //Appropriate suppression of unchecked warning 在尽可能小的范围使用禁止警告
    public E pop() 
        if (size == 0) 
            throw new EmptyStackException();
        
        //push requires elements to be of type E,so cast is correct
        @SuppressWarnings("unchecked") E result =
                (E) element[--size];
        element[size] = null;
        return result;
    

    public boolean isEmpty() 
        return size == 0;
    

    private void ensureCapacity() 
        if (element.length == size) 
            element = Arrays.copyOf(element, 2 * size + 1);
        
    

    public static void main(String[] args) 
        StackImprove2<String> stringStack=new StackImprove2<>();
        List<String> stringList=new ArrayList<>();
        stringList.add("cc");
        stringList.add("dd");
        for (String s : stringList) 
            stringStack.push(s);
        
        System.out.println(stringStack.element.length);
        while (!stringStack.isEmpty())
            System.out.println(stringStack.pop().toUpperCase());
        
    



消除通用数组创建的这两种技术都有它们的支持者。

  • 第一个更具有可读性:数组声明为E[]类型,清楚地表明它只包含E实例。它也更简洁:在典型的泛型类中,你可以在代码中的很多地方读取数组;第一种技术只需要一次转换(创建数组的位置)
  • 第二种技术每次读取数组元素时都需要单独的转换。

因此,优选第一种,并且在实践中更常用第一种。但是,它会导致堆污染(heap pollution)(第32项):数组的运行时类型与其编译时的类型不匹配(除非E恰好是Object)。这使得一些程序猿非常不安,他们选择第二种技术,尽管在这种情况下堆污染是无害的。

下面的程序示范了泛型Stack类的使用。程序以相反的顺序打印出它的命令行参数,并转换成大写字母。如果要在从堆栈中弹出的元素上调用String的toUpperCase方法,并不需要显式的转换,并且会确保自动生成的转换会成功:

// Little program to exercise our generic Stack
public static void main(String[] args) 
    Stack<String> stack = new Stack<>();
    for (String arg : args)
        stack.push(arg);
    while (!stack.isEmpty())
        System.out.println(stack.pop().toUpperCase());


上面的示例可能看起来与第28项相矛盾,第28项鼓励优先使用列表而不是数组。在泛型中使用列表并不总是可行或可取的。Java并不是生来就支持列表,因此有些泛型如ArrayList,则必须在数组上实现。为了提升性能,其他泛型如HashMap也在数组上实现。

绝大多数泛型与我们的Stack示例类似,因为它们的类型参数没有限制:你可以创建Stack、Stack<\\int[]>、Stack<List>,或者任何其他对象引用类型的Stack。

  • 注意不能创建基本类型的Stack:企图创建Stack<\\int>或者Stack<\\double>会产生一个编译时错误。这是Java泛型系统根本的局限性。
  • 你可以通过使用基本包装类型(boxed primitive type)来避开这条限制(第61项)。

总结

总而言之,使用泛型比使用需要在客户端代码中进行转换的类型来得更加安全,也更加容易。在设计新类型的时候,要确保它们不需要这种转换就可以使用。这通常意味着要把类做成泛型。如果你现在有任何类型应该是通用的但却不是通用的,就把现有的类型都泛型化。这对于这些类型的新用户来说会变得更加轻松,又不会破坏现有的客户端(第26项)。

2、优先考虑泛型方法

静态工具方法尤其适合于泛型化。Collections中的所有“算法”方法(例如binarySearch和sort)都泛型化了。

编写泛型方法与编写泛型类型相类似。例如下面这个方法,它返回两个集合的联合:

 // Use raw types - unacceptable! (Item 23)
 
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);

为了修正这些警告,使方法变成是类型安全的,要将方法声明修改为声明一个类型参数,表示这三个集合的元素类型(两个参数和一个返回值),并在方法中使用类型参数。

在这个示例中,类型参数列表为,返回类型为Set。类型参数的命名惯例与泛型方法以及泛型的相同:

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

至少对于见到那的泛型方法而言,就是这么回事了。现在该方法编译时不会产生任何警告,并提供了类型安全性,也更容易使用。以下是一个执行该方法的简单程序。程序中不包含转换,编译时不会有错误或者警告:

// Simple program to exercise generic method
 
public static void main(String[] args) 
 
    Set<String> guys = new HashSet<String>(
 
        Arrays.asList("Tom" , "Dick" , "Harry"));
 
    Set<String> stooges = new HashSet<String>(
 
        Arrays.asList("Larry" , "Moe" , "Curly"));
 
    Set<String> aflCio = union(guys , stooges);
 
    System.out.println(aflCio);
 

运行这段程序时,会打印出[Moe , Harry , Tom , Curly , Larry , Dick]。元素的顺序是依赖于实现的。

union方法的局限性在于,三个集合的类型(两个输入参数和一个返回值)必须全部相同。利用有限制的通配符类型(bounded wildcard type),可以使这个方法变得更加灵活。

泛型方法的一个显著特性是,无需明确指定类型参数的值,不像调用泛型构造器的时候是必须指定的。编译器通过检查方法参数的类型来计算类型参数的值。

  • 对于上述的程序而言,编译器发现union的两个参数都是Set<String>类型,因此知道类型参数E必须为String。这个过程称作类型推导(type inference)

可以利用泛型方法调用所提供的类型推导,使创建参数化类型实例的过程变得更加轻松。提醒一下:在调用泛型构造器的时候,要明确传递类型参数的值可能有点麻烦。类型参数出现在了变量声明的左右两边,显得有些冗余:

// Parameterized type instance creation with constructor
 
Map<String , List<String>> anagrams = new HashMap<String , List<String>>();

为了消除这种冗余,可以编写一个泛型静态工厂方法(generic static factory method),与想要使用的每个构造器相对应。例如,下面是一个与无参的HashMap构造器相对应的泛型静态工厂方法:

// Generic static factory
 
public static <K , V> HashMap<K , V> newHashMap() 
 
    return new HashMap<K , V>();
 

通过这个泛型静态工厂方法,可以用下面这段简洁的代码来取代上面那个重复的声明:

// Parameterized type instance creation with static factory
 
Map<String , List<String>> anagrams = newHashMap();

相关的模式是泛型单例工厂(generic singleton factory)。有时,会需要创建不可变但又适合于许多不同类型的对象。由于泛型是通过擦除实现的,可以给所有必须的类型参数使用单个对象,但是需要编写一个静态工厂方法,重复的给每个必要的类型参数分发对象。这种模式最常用于函数对象,如Collections.reverseOrder,但也适用于像Collections.emptySet这样的集合。

假设有一个接口,描述了一个方法,该方法接受和返回某个类型T的值:

public interface UnaryFunction<T> 
 
    T apply(T arg);
 

现在假设要提供一个恒等函数(identity function)。如果在每次需要的时候都重新创建一个,这样会很浪费,因为他是无状态的(stateless)。如果泛型被具体化了,每个类型都需要一个恒等函数,但是他们被擦除以后,就只需一个泛型单例。请看以下示例:

// Generic singleton factory pattern
 
private static UnaryFunction<Object> IDENTITY_FUNCTION = new UnaryFunction<Object>()
 
    public Object apply(Object arg)  return arg; 
 
;
 
// IDENTITY_FUNCTION is stateless and its type parameter is
 
// unbounded so it's safe to share one instance across all types.
 
@SuppressWarnings("unchecked")
 
public static <T> UnaryFunction<T> identityFunction() 
 
    return (UnaryFunction<T>) IDENTITY_FUNCTION;
 


IDENTITY_FUNCTION转换成(UnaryFunction<T>),产生了一条未受检的转换警告,因为UnaryFunction<Object>对于每个T来说并非都是个UnaryFunction<T>。但是恒等函数很特殊:它返回未被修改的参数,因此我们知道无论T的值是什么,用它作为UnaryFunction<T>都是类型安全的。因此,我们可以放心的禁止由这个转换所产生的未受检转换警告。一旦禁止,代码在编译时就不会出现任何错误或者警告。

以下是一个范例程序,利用泛型单例作为UnaryFunction<String>UnaryFunction<Number>。像往常一样,他不包含转换,编译时没有出现错误或者警告:

// Sample program to exercise generic singleton
 
public static void main(String[] args) 
 
    String[] strings = "jute" , "hemp" , "nylon";
 
    UnaryFunction<String> sameString = identityFunction();
 
    for (String s : strings ) 
 
        System.out.println(sameString.apply(s));
 
    
 
    Number[] numbers = 1 , 2.0 , 3L;
 
 
    UnaryFunction<Number> sameNumber = identityFunction();
 
    for (Number n : numbers ) 
 
        System.out.println(sameNumber.apply(n));
 
    
 

虽然相对少见,但是通过某个包含该类型参数本身的表达式来限制类型参数是允许的,这就是递归类型限制(recursive type bound)。递归类型限制最普遍的用途与Comparable接口有关,它定义类型的自然顺序:

public interface Comparable<T> 
 
    int compareTo(T o);
 


类型参数T定义的类型,可以与实现Comparable<T>的类型的元素进行比较。实际上,几乎所有的类型都只能与他们自身的类型的元素相比较。因此,例如String实现Comparable<String>,Integer实现Comparable<Integer>,等等。

有许多方法都带有一个实现Comparable接口的元素列表,为了对列表进行排序,并在其中进行搜索,计算出它的最小值或者最大值,等等。要完成这其中的任何一项工作,要求列表中的每个元素要都能与列表中的每个其他元素相比较,换句话说,列表的元素可以互相比较(mutually comparable)。下面是如何表达这种约束条件的一个示例:

// Using a recursive type bound to express mutual comparability
 
public static <T extends Comparable<T>> T max(List<T> list) ...

类型限制<T extends Comparable<T>>,可以读作“针对可以与自身进行比较的每个类型T”,这与互比性的概念或多或少有些一致。

下面的方法就带有上述声明。他根据元素的自然顺序计算列表的最大值,编译时没有出现错误或者警告:

// Returns the maximum value in a list - uses recursive type bound
 
public static <T extends Comparable<T>> T max(List<T> list) 
 
    Iterator<T> i =list.iterator();
 
    T result = i.next();
 
    while (i.hasNext()) 
 
        T t = i.next();
 
        if (t.compareTo(result) > 0) result = t;
 
    
 
    return result;
 

总结

总而言之,泛型方法就像泛型一样,使用起来比要求客户端转换输入参数并返回值的方法来得更加安全,也更加容易。就像类型一样,你应该确保新方法可以不用转换就能使用,这通常意味着要将它们泛型化。并且就像类型一样,还应该将现有的方法泛型化,使新用户使用起来更加轻松,且不会破坏现有的客户端。

参考

1、建议:优先考虑泛型方法。
2、Effective Java 优先考虑泛型
3、Effective Java笔记第四章泛型第四节优先考虑泛型

effectivejava学习笔记之创建和销毁对象

一、考虑用静态工厂方法代替构造器1、此处的静态工厂方法是指返回指为类的对象的静态方法,而不是设计模式中的静态工厂方法。2、静态工厂方法的优势有:a、使用不同的方法名称可显著地表明两个静态工厂方法的不同,而... 查看详情

effectivejava学习笔记之所有对象都通用的方法

一、覆盖equals时请遵守通用约定1、满足下列任何一个条件时,不需要覆盖equals方法a、类的每个实例本质上都是唯一的。此时就是Object中equals方法所表达的含义。b、不关心类是否提供了“逻辑相等”的测试功能c、超类中覆... 查看详情

effectivejava学习笔记之不可实例化的类

在没有显式声明一个类的构造方法时,编译器会生成默认的无参构造方法,在设计工具类时,我们通常将方法设置成静态方法,以类名.方法名的形式调用,此时这个类就没有必要创建实例,我们知道抽象类不可以被实例化,但... 查看详情

java:effectivejava学习笔记之复合优先于继承(代码片段)

Java复合优先于继承复合优先于继承1、实现继承和接口继承2、在实际开发中继承的缺点2.1、子类依赖于其超类中特定功能的实现细节3、什么是复合?3.1、书上案例4、复合相比较于继承的优点和缺点5、何时使用继承,何时使用... 查看详情

java:java学习笔记之java单例模式的简单理解和使用(代码片段)

...单例的实现如下:2、懒汉式终极版本:volatile3、EffectiveJava1——静态内部类4、5.2EffectiveJava2——枚举参考Java单例模式1、饿汉式单例的实现如下://饿汉式实现publicclassSingleBprivatestaticfinalSingleBINSTA 查看详情

java:effectivejava学习笔记之避免使用终结方法(代码片段)

Java避免使用终结方法避免使用终结方法1、finalize()基本概念2、finalize()的执行过程3、为什么要避免覆盖并使用finalize方法?4、如果类中的资源确实需要被释放,我们应该怎么做?5、终结方法的利弊5.1、终结方法的好... 查看详情

java:effectivejava学习笔记之考虑实现comparable接口(代码片段)

Java考虑实现Comparable接口考虑实现Comparable接口1、Comparable接口2、为什么要考虑实现Comparable接口3、compareTo方法的通用约定4、何时以及如何实现Comparable接口4.1、多重比较5、实现Comparable接口所需满足的需求6、总结参考考虑实现Compa... 查看详情

java:effectivejava学习笔记之覆盖equals时请遵守通用约定(代码片段)

Java覆盖equals时请遵守通用约定覆盖equals时请遵守通用约定1、为什么要覆盖equals2、需要覆盖equals方法的时机2.1、不需要覆盖equals方法的情况2.2、需要覆盖equals方法的情况2.2.1、自反性2.2.2、对称性2.2.3、传递性2.2.4、一致性2.2.5、... 查看详情

java:effectivejava学习笔记之接口只用于定义类型类层次优于标签类(代码片段)

Java接口只用于定义类型1、接口只用于定义类型1.1、常量接口2、类层次优于标签类1、接口只用于定义类型当类实现接口时,接口就充当可以引用这个类的实例的类型。因此,类实现了接口,就表明可以对这个类的实... 查看详情

java:effectivejava学习笔记之列表优先于数组(代码片段)

Java列表优先于数组列表优先于数组1、协变与不可变类型2、运行时检验与编译器检验3、可具体化与不可具体化4、无法很好混用列表和数组5、案例分析参考列表优先于数组1、协变与不可变类型1、数组是协变类型,指继承关... 查看详情

rabbitmq学习笔记五:rabbitmq之优先级消息队列

...效2、RabbitMQ3.5以后才支持优先级队列代码在博客:RabbitMQ学习笔记三:Java实现RabbitMQ之与Spring集成 最后面有下载地址,只是做了少许改变,改变的代码如下:消费者spring-config.xml(还需要增加一个QueueListener监听器,代码就不... 查看详情

java:effectivejava学习笔记之请不要在新代码中使用原生态类型(代码片段)

Java请不要在新代码中使用原生态类型1、请不要在新代码中使用原生态类型参考1、请不要在新代码中使用原生态类型1、在没有泛型之前,从集合中读取到每一个对象都必须进行转换。如果有人不小心插入了类型错误的对象&#x... 查看详情

java:effectivejava学习笔记之避免创建不必要的对象(代码片段)

Java避免创建不必要的对象避免创建不必要的对象1、采用更合适的API或工具类减少对象的创建2、重用相同功能的对象3、小心自动装箱(autoboxing)4、用静态工厂方法而不是构造器5、正则表达式6、补参考避免创建不必要的... 查看详情

java:effectivejava学习笔记之消除过期对象引用(代码片段)

Java消除过期对象引用消除过期对象引用1、Java的垃圾回收机制2、Java中的内存泄露3、常见的内存泄露参考消除过期对象引用很多人可能在想这么一个问题:Java有垃圾回收机制,那么还存在内存泄露吗?答案是肯定的&#... 查看详情

java:effectivejava学习笔记之接口优于抽象类(代码片段)

Java接口优于抽象类接口优于抽象类1、接口和抽象类2、接口优点3、骨架类3.1、demo参考接口优于抽象类1、接口和抽象类Java中抽象类和接口的区别2、接口优点1、现有的类可以很容易的被更新,以实现新的接口。如果你前期编... 查看详情

java:effectivejava学习笔记之静态工厂方法的简单理解和使用(代码片段)

Java静态工厂方法静态工厂方法一、什么是静态工厂方法?二、静态工厂方法的优势1、静态工厂方法与构造器不同的第一优势在于,它们有名字2、静态工厂方法不用在每次调用的时候都创建一个新的对象3、静态工厂方法... 查看详情

java:effectivejava学习笔记之覆盖equals时总要覆盖hashcode(代码片段)

Java覆盖equals时总要覆盖hashcode覆盖equals时总要覆盖hashcode1.什么是hashcode方法?2.hashcode相等与对象相等之间的关系:(保证设计是规范的前提下)3.为什么要覆盖hashcode3.1、覆盖equals时总要覆盖hashCode3.2、如何在覆盖... 查看详情

java:effectivejava学习笔记之通过私有构造器强化不可实例化的能力(代码片段)

Java通过私有构造器强化不可实例化的能力通过私有构造器强化不可实例化的能力参考通过私有构造器强化不可实例化的能力并非所有的类都是需要实例化的。有时候,我们可能需要编写至包含静态方法和静态域的类。这些类... 查看详情