Effective-Java-Chapter5-泛型

https://github.com/clxering/Effective-Java-3rd-edition-Chinese-English-bilingual/blob/dev/Chapter-5/Chapter-5-Introduction.md
在这里插入图片描述

准则一 不要使用原始类型

首先来看一下什么是原始类型呢?
List 对应的原始类型是 List,那其实就是说不带参数化类型。
直接使用原始类型会有什么危害呢?上例子

List list = new ArrayList();
// 添加一个点对象
list.add(new Point(1, 2));
// 添加一个字符串
list.add("add String");
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
    Point next = (Point) iterator.next();
    System.out.println(next.x + " : " + next.y);
}

上面这个代码可以编译通过,但是运行的时候会报错,当然平时我们可能不会这样去写。之所以能够编译通过,是因为Java是伪泛型,通过擦写为Object来实现的,所以相当于往List《Object》里放数据。
在这里插入图片描述

public static void main(String[] args) {
      List<String> strings = new ArrayList<>();
      unsafeAdd1(strings, Integer.valueOf(42));
      unsafeAdd2(strings, Integer.valueOf(42));
      String s = strings.get(0); 
  }
  private static void unsafeAdd1(List list, Object o) {
      list.add(o);
  }
  private static void unsafeAdd2(List<Object> list, Object o) {
      list.add(o);
  }

在这里插入图片描述
为什么unsafeAdd2通不过编译呢?那是因为如果参数化类型写成Objet,List《String》不是List《Object》的子类,这一点千万要记住,译器会抛出错误,因为它不允许你将一个List《String》当作List《Object》使用。这是因为List《String》是一个更具体的类型,而《Object》是一个更通用的类型。
所以最佳实践是我们不使原始类型,也不是用List《Object》而是使用采用下面的方式:

// 这里为什么要Collection 呢?因为这样的话,我们的方法会更通用
public  static <E> void unsafeAdd4(Collection<E> target, E element) {
   target.add(element);
}

或者使用通配符:
这样做确实可以达到目的,但是我们一般不这么使用,我们一般来说要通用一点,但是这样写就是检查了一个类型,并不是一个通用的方法。

private static void unsafeAdd3(List<? super String> list, String o) {
    list.add(o);
}

也许你会这样写?

private static void safeAdd3(List<? super Object> list, Object o) {
    list.add(o);
}

这样是不是更通用了,答案是如果你还是像上面那样使用,编译通不过,问题就在于你传入的是List《String》,但是往里面添加的元素却是Object类型,类型是不匹配的。

补充知识,关于参数中使用通配符 ? extends 和 ? super 是有区别的:
如果说你是消费集合里面的元素要用extends,如果你要修改集合往里面添加或者删除则要用 super,什么意思呢?还是举个例子:

public static void lower(List<? extends Number> producer, List<? super Number> consumer) {
    // 消费数据用 extend 比如我们遍历数据就是消费,也就是读取数据 这里不在写代码进行演示
    
    // 报错 不允许添加因为不确定与实际类型是否兼容
    producer.add(Integer.valueOf(1));
    
    // 操作数据用super
    // 可以正常添加
    consumer.add(Integer.valueOf(1));
}

List<? extends Number> producer:表示这是一个列表,其中的元素类型是Number或其子类。我们不能确定具体是什么类型,但可以确定它一定是Number的一个子类。这就可能出现 实际类型为 javaList<Integer>、List<Double> 或 List<Byte> 会导致类型不兼容,比如我们的类型是Integer,如果允许操作的话,这个时候来一个Double怎么办呢?所以extends不允许我们进行操作。
List<? super Number> consumer:表示这是一个列表,其中的元素类型是Number或其父类。这可以包括Number类型或其超类(比如Object)。这就意味着Integer,Double都可以添加,因为Number是一个更通用的类型,类型是兼容的。

友情连接:
https://blog.csdn.net/qq_43259860/article/details/137127886
https://blog.csdn.net/qq_43259860/article/details/137032842

准则二 消除 unchecked 警告

如果不能消除警告,但是可以证明引发警告的代码是类型安全的,那么(并且只有在那时)使用 SuppressWarnings(“unchecked”) 注解来抑制警告。

准则三 list 优于数组

  • 理由一
    如果 Sub 是 Super 的一个子类型,那么数组类型 Sub[] 就是数组类型 Super[] 的一个子类型。
    但是List<sub> 和List<super>不存在这样的关系为什么呢?
    里氏替换原则(Liskov Substitution Principle,LSP)面向对象设计的基本原则之一。里氏替换原则指出:任何父类可以出现的地方,子类一定可以出现。
    你想想看比如java List<String> 如果是List<Object>的子类根据里氏替换原则是不是说不通啊。Sting的List只能添加Sting的类型也很明显不符合里氏替换原则。
// 数组类型 Sub[] 就是数组类型 Super[] 的一个子类型 所以下面这样写编译时可以通过的
Object object[] = new Long[1];
// 这样就导致了运行时异常
object[0] = "java";
// 编译无法通过
List<Object> list = new ArrayList<String>();

数组是具体化的 [JLS, 4.7]。这意味着数组在运行时知道并强制执行他们的元素类型。相比之下,泛型是通过擦除来实现的 [JLS, 4.6]。这意味着它们只在编译时执行类型约束,并在运行时丢弃(或擦除)元素类型信息。
由于这些基本差异创建泛型、参数化类型或类型参数的数组是非法的。因此,这些数组创建表达式都不是合法的:new List[]、new List[]、new E[]。

// (1) 不会通过编译
List<String>[] stringLists = new List<String>[1];
// (2)
List<Integer> intList = Arrays.asList(1);
// (3)
Object[] objects = stringLists;
// (4)
objects[0] = intList;
// (5)
String s = stringLists[0].get(0);

为什么呢?因为这样存在类型安全,假如第一步中的创建是合法的,那么由于前面提到的sub[] 和 super[]存在父子关系,所以第三步是合法的,那么在第四步我们这个操作完全没毛病吧,反正是一个对象类型的数组都可以存,但是使用的时候这就报错了ClassCastException。

  • 理由二
// Chooser - a class badly in need of generics!
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)];
  }
}

// A first cut at making Chooser generic - won't compile
public class Chooser<T> {
  private final T[] choiceArray;

  public Chooser(Collection<T> choices) {
    choiceArray = (T[]) choices.toArray();
  }

  // choose method unchanged
}


// List-based Chooser - typesafe
public class Chooser<T> {
    private final List<T> choiceList;

    public Chooser(Collection<T> choices) {
        choiceList = new ArrayList<>(choices);
    }

    public T choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceList.get(rnd.nextInt(choiceList.size()));
    }
}

我们依次来分析这三段代码,首先第一段我们可以实现随机选取一个对象,但是我们必须知道对象是什么类型,每次使用的时候都必须强转。所以我们有了第二段代码,进行了优化,更加的通用,但是第二段代码在(T[]) 这个步骤的时候对得到一个unchecked,但是其实这个警告我们可以忽略,因为我们明确的知道此处不会存在类型转换安全。但是如果要进一步消除这个警告,我们就可以按照第三段代码的方式来写。

准则四 优先使用泛型

但是我们在泛型的使用过程中存在一些问题,让我来看一个例子。

public class Stack<E> {
    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 reference
        return result;
    } ... // no changes in isEmpty or ensureCapacity
}

问题在哪里?
elements = new E[DEFAULT_INITIAL_CAPACITY]; 泛型是不允许实例化的,为了解决这个问题我们有两种方案。

  • 方案一
    尽管这里会得到unchecked异常,但是我们自己清楚这里这样做是安全的,所以我们可以忽略这个警告。
@SuppressWarnings("unchecked")
public Stack() {
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
  • 方案二:
    尽然是栈,那我们参考一下Java源码的实现,我们可以看到源码中的Stack继承了Vector
    在这里插入图片描述
    我们可以清晰的看到,它定义的是对象类型的数组。那么类型转换是在那里完成的呢?我们可以看到添加的时候是直接进行了添加:
    在这里插入图片描述

在这里插入图片描述
我们可以看看pop方法,可以看到最终取元素的逻辑如下:
在这里插入图片描述

那就是在每一个元素使用的时候进行转换。这里依然可能存在类型安全的问题,同样的我们清晰的知道是允许的,所以方法上申明了一个@SuppressWarnings(“unchecked”),这也是符合准则二。

准则五 优先使用泛型方法

使用泛型方法是为了更通用,这里可以参考源码,比如UnaryOperator:

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {

    /**
     * Returns a unary operator that always returns its input argument.
     *
     * @param <T> the type of the input and output of the operator
     * @return a unary operator that always returns its input argument
     */
    static <T> UnaryOperator<T> identity() {
        return t -> t;
    }
}

又比如经典的Comparable:

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

准则六 使用有界通配符增加 API 的灵活性

那为什么通配符能够增加灵活性呢?

   public static void main(String[] args) {
        DemoStack<Number> stack = new DemoStack<>();
        stack.push(new Integer(1));
        Iterable<Integer> integers = Arrays.asList(1, 2, 3);
        stack.pushAll(integers);
    }

    public static class DemoStack<E> {
        private Object[] data;
        private int idx = 0;

        public DemoStack() {
            data = new Object[10];
        }

        public void push(E e) {
            this.data[idx++] = e;
        }
        public void pushAll(Iterable<E> src) {
            for (E e : src)
                push(e);
        }
    }

我们可以看到这样的代码有个缺陷:
在这里插入图片描述
那如何解决呢?

public void pushAll(Iterable<? extends E> src) {
   for (E e : src)
       push(e);
}

那如果是进行,我们把元素取出来添加到另一个集合,因为这里是要进行操作,所以只有当元素是E类或者是E的父类才允许操作,关于为什么可以看前面准则一,举的例子:

public void popAll(Collection<? super E> dst) {
  while (!isEmpty())
    dst.add(pop());
}

生产者使用 extends,消费者使用 super(PECS)。
在我们的 Stack 示例中,pushAll 的 src 参数生成 E 的实例供 Stack 使用,因此 src 的适当类型是 Iterable<? extends E>;popAll 的 dst 参数使用 Stack 中的 E 实例,因此适合 dst 的类型是 Collection<? super E>。

还要记住,所有的 comparable 和 comparator 都是消费者。Comparables 始终是消费者,所以一般应优先使用 Comparable<? super T> 而不是 Comparable,比较器也是如此;因此,通常应该优先使用 Comparator<? super T> 而不是 Comparator。

如果它是一个无界类型参数,用一个无界通配符替换它;无解参数会存在问题,下面的代码不会通过编译:

public static void swap(List<?> list, int i, int j) {
  list.set(i, list.set(j, list.get(i)));
}

问题是 list 的类型是 List<?>,你不能在 List<?> 中放入除 null 以外的任何值。但是你可以liyong下面这种写法来实现这个功能。

public static void swap(List<?> list, int i, int j) {
  swapHelper(list, i, j);
}
// Private helper method for wildcard capture
private static <E> void swapHelper(List<E> list, int i, int j) {
  list.set(i, list.set(j, list.get(i)));
}

准则七 考虑类型安全的异构容器

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));
  }
}

如果一个容器能够同时存储Integer、String和其他类型的对象,那么它就是一个异构容器。
比如我们来看Favorites 这个类,与普通 Map 不同,所有键都是不同类型的,也就是可以存储不同的类型。
上面这样做还是有一定的缺陷,因为类型的一致是通过键和值类确定的,那么如果值的类型可能会和键不一样,如果使用人员想要这样做的话。所以我们为了保障类型安全:

// 这样就能保证类型的安全 所以推荐实践过程中可以选用异构安全的容器
public <T> void putFavorite(Class<T> type, T instance) {
    favorites.put(type, type.cast(instance));
}

同时这里还有一个缺陷:
你可以存储的 Favorites 实例类型为 String 类型或 String[],但不能存储 List。原因是你不能为 List 获取 Class 对象,List.class 是一个语法错误,这也是一件好事。List 和 List 共享一个 Class 对象,即 List.class。

  • 23
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《Effective Java第三版》是由Joshua Bloch所著的一本Java编程指南。这本书是基于第二版的更新版本,目的是给Java程序员提供一些最佳实践和经验,以编写高效、可维护和可靠的Java代码。 这本书共分为15个章节,每个章节都讲解了一个与Java开发有关的重要主题。比如,章节一讲述了使用静态工厂方法代替构造器的优点,章节二则介绍了如何用Builder模式来构建复杂的对象。此外,书中还提及了Java对象的等价性、覆盖equals方法和hashCode方法、避免创建不必要的对象、使用泛型、枚举、lambda表达式等等。 《Effective Java第三版》通过具体的代码示例和清晰的解释来说明每个主题的关键概念,使读者能够更好地理解和应用。此外,书中还提供了一些实用的技巧和技术,例如避免使用原始类型、尽量使用接口而非类来定义类型等。 总的来说,这本书提供了很多实用的建议和技巧,可以帮助Java开发者写出高质量的代码。无论是初学者还是有经验的开发者,都可以从中受益匪浅。无论你是打算从头开始学习Java编程,还是已经有一定经验的开发者,这本书都是值得推荐的读物。 ### 回答2: 《Effective Java 第三版》是由Joshua Bloch 所著的一本Java编程指南,是Java程序员必读的经典之作。该书共包含90个条目,涵盖了各种Java编程的最佳实践和常见问题的解决方法。 本书分为多个部分,每个部分都侧重于一个特定的主题。作者探讨了Java编程中的各种问题和挑战,并提供了解决方案和建议。这些建议包括如何选择和使用合适的数据结构和算法,如何设计高效的类和接口,如何处理异常和错误,以及如何编写可读性强的代码等等。 《Effective Java 第三版》还关注了Java编程中的性能优化和安全性问题。作者强调了遵循Java语言规范、使用标准库、防范常见安全漏洞等重要原则。此外,本书还介绍了Java 8及其后续版本的新特性和用法,如Lambda表达式、流式编程和Optional类等。 这本书的特点之一是每个条目都独立于其他条目,可以单独阅读和理解。每个条目开头都有一个简洁的总结,让读者能够快速掌握主要观点。此外,书中还有大量的示例代码和解释,帮助读者更好地理解和运用所学知识。 总的来说,《Effective Java 第三版》是一本非常实用和全面的Java编程指南。它适用于各个层次的Java程序员,无论是初学者还是有经验的开发人员,都可以从中获得宝贵的经验和知识。无论是编写高质量的代码、优化性能还是确保安全性,这本书都是一本不可或缺的参考书籍。 ### 回答3: 《Effective Java 第3版(中文版)》是由 Joshua Bloch 所著的一本关于使用 Java 编程语言的指南书。该书是对 Java 语言的最佳实践的详尽描述,为中高级 Java 开发人员提供了许多实用的建议和技巧。 该书的主要内容包括Java 语言的优雅编程风格、类和接口的设计、Lambda 表达式和流的使用、泛型、异常和并发编程等方面的最佳实践。 在《Effective Java 第3版(中文版)》中,许多传统的 Java 开发中的陷阱、常见错误和不良习惯都得到了深入的剖析和解答。它不仅提供了可供开发人员参考的示例代码,还解释了为什么某种方式是有问题的,以及如何更好地进行改进。 该书的深度和广度非常适合正在努力提高 Java 编程技能的开发人员。它涵盖了多个关键领域,为读者提供了在实际项目中解决常见问题的方法和思路。 此外,《Effective Java 第3版(中文版)》还介绍了最新版本的一些特性和改进。例如,它详细说明了如何正确地使用 Java 8 中新增的 Lambda 表达式和流,以及如何充分利用 Java 9、10 和 11 中的新功能。 总之,这本书是 Java 开发人员必备的指南之一。通过深入理解和应用书中的实践建议,读者可以更加高效地编写、优化和维护 Java 代码。无论是想提升职业技能还是在项目中减少错误和问题,这本《Effective Java 第3版(中文版)》都是一本非常有帮助的参考书。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值