Effective java 3th-读书笔记

第二章 创建和销毁对象

1、用静态工厂方法替代构造器

        它只是一个返回类实例的静态方法,不同于设计模式的工厂模式。

优点

1.静态工厂方法有名称,易读、易用

        一个类只能有一个带有指定签名的构造器,由于静态工厂方法有名称,所以它们不受上述限制。

public static Boolean valueOf(boolean b) {
    return b? Boolean.TRUE : Boolean.FALSE;
}

2.可以灵活控制新对象的创建, 如单子模式、享元模式。

         方法签名方法的名字和参数列表,方法签名不包括方法的返回类型

         享元模式:(Flyweight Pattern)运用共享技术有效的支持大量细粒度的对象。

3.可以返回原返回类型的任何子类型的对象。

4.所返回的对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值。

5.方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在。

缺点:

1. 类如果不含公有的或者受保护的构造器,就不能被子类化。

  2.程序员很难发现它们,在API 文档中,它们没有像构造器那样在API 文档中明确标识出来, 因此对于提供了静态工厂方法而不是构造器的类来说,要想查明如何实例化一个类是非常困难的。

2条:遇到多个构造器参数时要考虑使用构建器

静态工厂和构造器有个共同的局限性,不能很好地扩展到大量的可选参数

1. 重叠构造器( telescoping cons tructor )模式,在这种模式下,提供的第一个构造器只有必要的参数,第二个构造器有一个可选参数,第三个构造器有两个可选参数,依此类推,最后一个构造器包含所有可选的参数。

但是当有许多参数的时候,客户端代码会很难缩写,并且仍然较难以阅读

2.JavaBeans 模式,在这种模式下,先调用一个无参构造器来创建对象,然后再调用setter 方法来设置每个必要的参数,以及每个相关的可选参数。

属性不可控

3.建造者模式。它不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂),得到一个buil der 对象。然后客户端在bui lder 对象上调用类似于setter 的方法,来设置每个相关的可选参数。最后客户端调用无参的build 方法来生成通常是不可变的对象。

        如果类的构造器或者静态工厂中具有多个参数,设计这种类时, Builder模式就是一种不错的选择, 特别是当大多数参数都是可选或者类型相同的时候。与使用重叠构造器模式相比,使用Bui lder 模式的客户端代码将更易于阅读和编写,构建器也比JavaBeans 更加安全。

3 条:用私有构造器或者枚举类型强化Singleton 属性

        静态实例属性、静态实例方法、枚举

4 条:通过私有构造器强化不可实例化的能力

public class UtilityClass {

    // Suppress default constructor for noninstantiability
    private UtilityClass() {  //副作用,它使得该类不能被子类化。
        throw new AssertionError();//避免内部调用构造器
    } ... // Remainder omitted
}

5 条:优先考虑依赖注人来引用资源

        依赖注入( dependency injection )的一种形式:词典( dictionary )是拼写检查器的一个依赖( depend ency ),在创建拼写检查器时就将词典注入( injected )其中

// Dependency injection provides flexibility and testability
public class SpellChecker {

    private final Lexicon dictionary;

    public SpellChecker(Lexicon dictionary) {
        this.dictionary = Objects.requireNonNull(dictionary);
    }

    public boolean isValid(String word) { ... }
    public List<String> suggestions(String typo) { ... }

}

静态工具类和Singleton 类不适合于需要引用底层资源的类

6 条:避免创建不必要的对象

通常优先使用静态工厂方法而不是构造器,以避免创建不必要的对象。

如:

1.静态工厂方法Boolean. valueOf (String )几乎总是优先于构造器Boolean(String )

注意, 构造器Boolean(String )在Java 9 中已经被废弃了。

2. String s = new String("bikini"); // DON'T DO THIS!

           改进后版本: String s = "bikini";

3. // Performance can be greatly improved!

static boolean isRomanNumeral(String s) {
    return s.matches("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}

改进后版本

// Reusing expensive object for improved performance
public class RomanNumerals {
    private static final Pattern ROMAN = Pattern.compile(
            "^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
    static boolean isRomanNumeral(String s) {
        return ROMAN.matcher(s).matches();
    }
}

注意,与本条目对应的是第50 条中有关“保护性拷贝”( defensive copying )的内容。在提倡使用保护性拷贝的时候,因重用对象而付出的代价要远远大于因创建重复对象而付出的代价。必要时如果没能实施保护性拷贝,将会导致潜在的Bug 和安全漏洞;而不必要地创建对象则只会影响程序的风格和性能。

7 条:消除过期的对象引用

1、程序中过期引用,清除过期引用:  elements[size] = null; // Eliminate obsolete reference

2、缓存的生命周期,长期需定期清理

3、监听器和回调:确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用( weakreference )

8 条:避免使用终结方法和清除方法

1、终结方法(Finalizer):如果忽略在终结过程中被抛出来的未被捕获的异常,该对象的终结过程也会终止。正常情况下,未被捕获的异常将会使线程终止,并打印出战轨迹( Stack Trace ),但是,如果异常发生在终结方法之中,则不会如此,甚至连警告都不会打印出来。清除方法没有这个问题,因为使用清除方法的一个类库在控制它的线程。

     java中添加 清除方法(cleaner),清除方法没有终结方法那么危险,但仍然是不可预测、运行缓慢,一般情况下也是不必要的。

2、建议用try-with-resources关闭销毁不用的对象,

9 条: try-with-resources 优先于try-finally

1、try-with-resources java7引入,要使用这个构造的资源,必须先实现AutoCloseable 接口其中包含了单个返回void 的close 方法

// try-finally is ugly when used with more than one resource!
static void copy(String src, String dst) throws IOException {

    InputStream in = new FileInputStream(src);
    try {

        OutputStream out = new FileOutputStream(dst);
        try {
            byte[] buf = new byte[BUFFER_SIZE];
            int n;
            while ((n = in.read(buf)) >= 0)
                out.write(buf, 0, n);
        } finally {
            out.close();
        }
    } finally {
        in.close();
    }
}

// try-with-resources on multiple resources - short and sweet
static void copy(String src, String dst) throws IOException {
    try (InputStream in = new FileInputStream(src);
        OutputStream out = new FileOutputStream(dst)) {
        byte[] buf = new byte[BUFFER_SIZE];
        int n;
        while ((n = in.read(buf)) >= 0)
            out.write(buf, 0, n);
       }
}

在处理必须关闭的资源时,始终要优先考虑用try- with-resources ,而不是用try-finally 。代码简洁且不会摸出重要异常

第三章 对于所有对象都通用的方法

10 条:覆盖equals 时请遵守通用约定

覆盖equals,通常应用于值类,如String、Integer

枚举不需要覆盖equals,对于这种类,逻辑相同 和 对象等同是一回事。

equals 方法实现了等价关系( equi va lence relation ),其属性如下:

 自反性( reflexive ) : 对于任何非null 的引用值x x . equals(x )必须返回true 。

对称性( symmetric ):对于任何非null 的引用值x 和y ,当且仅当y.equals(x )返回true 时', x.equals(y )必须返回true 。

传递性( transitive ) : 对于任何非null 的引用值x 、y 和z ,如果x.equals(y )返回true ,并且y.equals(z )也返回true ,那么x.equals(z )也必须返回true

一致性( consistent ) : 对于任何非nu ll 的引用值x 和y ,只要equals 的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y )就会一致地返回true,或者一致地返回false 。

对于任何非null 的引用值x, x.equals (null )必须返回false 。

11 条:覆盖equals 时总要覆盖hashCode

在每个覆盖了equals 方法的类中, 都必须覆盖hashCode 方法。如果不这样做的话,就会违反hashCode 的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作。下面是约定的内容,摘自Object 规范:

  • 在应用程序的执行期间,只要对象的equals 方法的比较操作所用到的信息没有被修改,那么对同一个对象的多次调用, hashCode 方法都必须始终返回同一个值。在一个应用程序与另一个程序的执行过程中,执行hashCode 方法所返回的值可以不一致。
  • 如果两个对象根据equals(Object )方法比较是相等的,那么调用这两个对象中的hashCode 方法都必须产生同样的整数结果。
  • 如果两个对象根据equals(Object )方法比较是不相等的,那么调用这两个对象中的hashCode 方法,则不一定要求hashCode 方法必须产生不同的结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表( hashtable )的性能。

计算hash值采用31 有个很好的特性,用移位和减法来代替乘法,可以得到更好的性能:

31 *i = ( i < < 5 ) - i 。

31 = 11111,  31*2-2 = 1000000 -10=111110=62

现代的虚拟机可以自动完成这种优化。

hashCode案例:

1、// Typical hashCode method

@Override public int hashCode() {

    int result = Short.hashCode(areaCode);
    result = 31 * result + Short.hashCode(prefix);
    result = 31 * result + Short.hashCode(lineNum);
    return result;
}

2、Objects 类有一个静态方法,它带有任意数量的对象,并为它们返回一个散列码。性能比自己的写的方法慢。

// One-line hashCode method - mediocre performance

@Override public int hashCode() {
    return Objects.hash(lineNum, prefix, areaCode);
}

3、如果一个类是不可变的,并且计算散列码的开销也比较大, 就应该考虑把散列码缓存在对象内部,而不是每次请求的时候都重新计算散列码。Jaa 类库中许多类,比如String 和Integer ,都可以把它们的hashCode 方法返回的确切

// hashCode method with lazily initialized cached hash code

private int hashCode; // Automatically initialized to 0

@Override public int hashCode() {

    int result = hashCode;

    if (result == 0) {
        result = Short.hashCode(areaCode);    
        result = 31 * result + Short.hashCode(prefix);
        result = 31 * result + Short.hashCode(lineNum);
        hashCode = result;
    } 
    return result;
}

        不要试图从散列码计算中排除掉一个对象的关键域来提高性能。

12 条:始终要覆盖toString

         提供好的t。String 实现可以便类用起来更加舒适,使用了这个类的系统也更易于调试。

         在实际应用中, toString 方法应该返回对象中包含的所有值得关注的信息;可以指定具体类的格式,如电话号码,指定格式的好处是,它可以被用作一种标准的、明确的、适合人阅读的对象表示法,通常最好再提供一个相匹配的静态工厂或者构造器,以便程序员可以很容易地在对象及其字符串表示法之间来回转换。指定toString 返回值的格式也有不足之处:如果这个类已经被广泛使用,就必须始终如一地坚持这种格式。如果不指定格式,就可以保留灵活性,便于在将来的发行版本中增加信息,或者改进格式。

无论是否决定指定格式,都应该在文档中明确地表明你的意图。

13 条: 谨慎地覆盖clone

         Cloneable决定了Object中受保护的clone 方法实现的行为:如果一个类实现了Cloneable, Object 的clone方法就返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedException 异常。事实上,实现Cloneable 接口的类是为了提供一个功能适当的公有的clone 方法。

        Java 支持协变返回类型( covariant return type ) 。换句话说,目前覆盖方法的返回类型可以是被覆盖方法的返回类型的子类了

        Entry 类中的深度拷贝方法递归地调用它自身,以便拷贝整个链表(它是链表的头节点) 。虽然这种方法很灵活,如果散列桶不是很长, 也会工作得很好,但是,这样克隆一个链表并不是一种好办法,因为针对列表中的每个元素,它都要消耗一段枝空间。如果链表比较长,这很容易导致枝溢出。为了避免发生这种情况,可以在deepCopy 方法中用迭代( iteration )代替递归( recursion ) :

// Recursively copy the linked list headed by this Entry
Entry deepCopy() {
    return new Entry(key, value,next == null ? null : next.deepCopy());
}

// Iteratively copy the linked list headed by this Entry
Entry deepCopy() {
    Entry result = new Entry(key, value, next);
    for (Entry p = result; p.next != null; p = p.next)
    p.next = new Entry(p.next.key, p.next.value, p.next.next);
    return result;
}

        对象拷贝的更好的办法是提供一个拷贝构造器( copy constructor)或拷贝工厂( copy factory ); 最好利用clone 方法复制数组。

14 条:考虑实现Comparable 接口

在下面的代码中,依赖于 String 类实现了 Comparable 接口,去除命令行参数输入重复的字符串,并按照字母顺序排序, TreeSet中元素实现了 Comparable接口,根据comparaTo方法返回值进行排序

public class WordList {

     public static void main(String[] args) {
        Set<String> s = new TreeSet<>();
        Collections.addAll(s, args); 
        System.out.println(s);
     }
}

compareTo 方法的通用约定与equals 方法的约定相似:

      将这个对象与指定的对象进行比较。当该对象小于、等于或大于指定对象的时候,分别返回一个负整数、零或者正整数。如果由于指定对象的类型而无法与该对象进行比较,则抛出ClassCastException 异常。

       如果你想为一个实现了Comparable 接口的类增加值组件,请不要扩展这个类;而是要编写一个不相关的类,其中包含第一个类的一个实例。然后提供一个“视图”( view )方法返回这个实例。这样既可以让你自由地在第二个类上实现compare To 方法,同时也允许它的客户端在必要的时候,把第二个类的实例视同第一个类的实例

        集合接口(Collection, Set Map )的通用约定是按照equals方法来定义的,但是有序集合使用了compareTo 方法而不是equals 方法

// Comparable with comparator construction methods

private static final Comparator<PhoneNumber> COMPARATOR =
    comparingInt((PhoneNumber pn) -> pn.areaCode)
        .thenComparingInt(pn -> pn.prefix)
        .thenComparingInt(pn -> pn.lineNum);

public int compareTo(PhoneNumber pn) {
    return COMPARATOR.compare(this, pn);
}

34 条: 用enum 代替int 常量

// Enum type that switches on its own value - questionable

public enum Operation {

    PLUS, MINUS, TIMES, DIVIDE;

    // Do the arithmetic operation represented by this constant

    public double apply(double x, double y) {
        switch(this) {
            case PLUS: return x + y;
            case MINUS: return x - y;
            case TIMES: return x * y;
            case DIVIDE: return x / y;
        }
        throw new AssertionError("Unknown op: "+this);
    }
}

        有一种更好的方法可以将不同的行为与每个枚举常量关联起来:在枚举类型中声明一个抽象的appl y 方法,并在特定于常量的类主体( constant-specific class body )中,用具体的方法覆盖每个常量的抽象apply 方法。这种方法被称作特定于常量的方法实现( constant-specific method implementation ),特定于常量的方法实现可以与特定于常量的数据结合起来。 :

枚举类型中的抽象方法必须被它的所有常量中的具体方法所覆盖。


// Enum type with constant-specific class bodies and data

public enum 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;
    Operation(String symbol) { this.symbol = symbol; }

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

    public abstract double apply(double x, double y);

}

每当需要一组固定常量.并且在编译时就知道其成员的时候,就应该使用枚举

42 条: Lambda 优先于匿名类

        在Java 8 中,增加了函数接口( functional interface )、Lambda 和方法引用( methodreference ),使得创建函数对象(且mction object )变得很容易。与此同时,还增加了StreamApi,为处理数据元素的序列提供了类库级别的支持。

       自从1997 年发布JDK 1.1 以来,创建函数对象的主要方式是通过匿名类。下面是一个按照字符串的长度对字符串列表进行排序的代码片段,它用一个匿名类创建了排序的比较函数(加强排列顺序):

函数对象:用带有单个抽象方法的接口(或者几乎不用的抽象类)作为函数类型( function type ,表示函数或者要采取的动作

// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return Integer.compare(s1.length(), s2.length());
    } 
});

        在Java 8 中,允许利用Lambda 表达式( Lambda expression ,简称Lambda )创建这些接口的实例。Lambda 类似于匿名类的函数,但是比它简洁得多。以下是上述代码用Lambda 代替匿名类之后的样子。

// Lambda expression as function object (replaces anonymous class)
Collections.sort(words,  (s1, s2) -> Integer.compare(s1.length(), s2.length()));

         注意, Lambda 的类型( Comparator<String >)、其参数的类型( s 1 和s2 ,两个都是String )及其返回值的类型( int ),都没有出现在代码中。编译器利用一个称作类型推导( type inference )的过程,根据上下文推断出这些类型。

        Lambda 限于函数接口。如果想创建抽象类的实例,可以用匿名类来完成,而不是用Lambda。

        Lambda 无法获得对自身的引用。在Lambda 中,关键字this 是指外围实例,这个通常正是你想要的。在匿名类中,关键字this 是指匿名类实例。

        Lambda 没有名称和文档;如果一个计算本身不是自描述的, 或者超出了几行, 那就不要把它放在一个Lambda 中。对于Lambda而言,一行是最理想的, 三行是合理的最大极限。如果违背了这个规则,可能对程序的可读性造成严重的危害。

43 条:方法引用优先于Lambda

        与匿名类相比, Lambda 的主要优势在于更加简洁。Java 提供了生成比Lambda 更简洁函数对象的方法: 方法引用( mothod reference ) 。

map.merge(key, 1, (count, incr) -> count + incr);

       这行代码中使用了merge 方法,这是Java 8 版本在Map 接口中添加的。如果指定的键没有映射, 该方法就会插入指定值;如果有映射存在, merge 方法就会将指定的函数应用到当前值和指定值上,并用结果覆盖当前值。这行代码代表了merge 方法的典型用例。

        从Java 8 开始, Integer(以及所有其他的数字化基本包装类型)提供了一个名为sum 的静态方法,它的作用也同样是求和。我们只要传人一个对该方法的引用,就可以更轻松地得到相同的结果:

map.merge(key ,1, Integer:: sum );

使用方法引用通常能够得到更加简短、清晰的代码。如果Lambda 太长,或者过于复杂,还有另一种选择: 从Lambda 中提取代码,放到一个新的方法中,并用该方法的一个引用代替Lambda。

只要方法引用更加简洁、清晰,就用方法引用;如果方法引用并不简洁,就坚持使用Lambda 。

service.execute(GoshThisClassNameIsHumongous::action);
service.execute(() -> action());

 

44 条:坚持使用标准的函数接口

        只要标准的函数接口能够满足需求,通常应该优先考虑,而不是专门再构建一个新的函数接口。

        java.util.Function 中共有43 个接口。别指望能够全部记住它们,但是如果能记住其中6 个基础接口,必要时就可以推断出其余接口了。基础接口作用于对象引用类型。Operator 接口代表其结果与参数类型一致的函数。Predicate接口代表带有一个参数并返回一个boolean 的函数。Function 接口代表其参数与返回的类型不一致的函数。Supplier 接口代表没有参数并且返回(或“提供”)一个值的函数。最后, Consumer 代表的是带有一个函数但不返回任何值的函数,相当于消费掉了其参数。这6 个基础函数接口概述如下:

UnaryOperator和BinaryOperator 分别对应单元算子和二元算子。

Interface

 Function Signature

 Example

UnaryOperator<T>

T apply(T t)

String::toLowerCase

BinaryOperator<T>

T apply(T t1, T t2)

BigInteger::add

Predicate<T>

boolean test(T t)

Collection::isEmpty

Function<T,R>

R apply(T t)

Arrays::asList

Supplier<T>

T get()

Instant::now

Consumer<T>

 void accept(T t)

System.out::println

千万不要用带包装类型的基础函数接口来代替基本函数接口,基本类型优于装箱基本类型

如果你所需要的函数接口与Comparator 一样具有一项或者多项以下特征, 则必须认真考虑自己编写专用的函数接口,而不是使用标准的函数接口:

  • 通用,并且将受益于描述性的名称。
  • 具有与其关联的严格的契约。
  • 将受益于定制的缺省方法。

        缺省方法:其允许在接口中增加新的方法,并在接口实现中可用,与正常方法不同,在方法声明之前加上default关键字,同时提供实现,因此无需修改实现类。

函数接口用@ FunctionalInterface 注解,它是是针对lambda设计的,他的接口只有一个抽象方法。

最后,不要在相同的参数位置,提供不同的函数接口来进行多次重载,这样可能在客户端导致歧义。

        总而言之,既然Java 有了Lambda ,就必须时刻谨记用Lambda 来设计API 。输入时接受函数接口类型,并在输出时返回之。一般来说,最好使用j ava.util.function.Function 中提供的标准接口,但是必须警惕在相对罕见的几种情况下,最好还是自己编写专用的函数接口。

45 条:谨慎使用Stream

        在Java 8 中增加了Stream API ,简化了串行或并行的大批量操作。这个API 提供了两个关键抽象: Stream (流)代表数据元素有限或无限的顺序, Stream pipeline (流管道)则代表这些元素的一个多级计算。Stream 中的数据元素可以是对象引用,或者基本类型值。它支持三种基本类型:int、long 和double 。

        一个Stream pipeline 中包含一个源Stream ,接着是0 个或者多个中间操作( intermediat巳operation )和一个终止操作( terminal operation ) 。每个中间操作都会通过某种方式对Stream进行转换,例如将每个元素映射到该元素的函数,或者过滤掉不满足某些条件的所有元素。所有的中间操作都是将一个Stream 转换成另一个Stream ,其元素类型可能与输入的Stream一样,也可能不同。终止操作会在最后一个中间操作产生的Stream 上执行一个最终的计算。例如将其元素保存到一个集合中,并返回某一个元素,或者打印出所有元素等。

        Stream pipeline 通常是lazy 的:直到调用终止操作时才会开始计算,对于完成终止操作不需要的数据元素,将永远都不会被计算。

        在默认情况下, Stream pipeline 是按顺序运行的。要使pipelin巳并发执行,只需在该pipeline 的任何Stream 上调用parallel 方法即可,但是通常不建议这么做

// Prints all large anagram groups in a dictionary iteratively

public class Anagrams {

    public static void main(String[] args) throws IOException {

        File dictionary = new File(args[0]);
        int minGroupSize = Integer.parseInt(args[1]);
        Map<String, Set<String>> groups = new HashMap<>();

        try (Scanner s = new Scanner(dictionary)) {

            while (s.hasNext()) {

                String word = s.next();
                groups.computeIfAbsent(alphabetize(word), (unused) -> new TreeSet<>()).add(word);
            }
        }

        for (Set<String> group : groups.values()) {
            if (group.size() >= minGroupSize)
                System.out.println(group.size() + ": " + group);
        }
    }

    private static String alphabetize(String s) {

        char[] a = s.toCharArray();
        Arrays.sort(a);
        return new String(a);

    }
}

        这里使用了Java 8 中新增的computeIfAbsent 方法。这个方法会在映射中查找一个键:如果这个键存在,该方法只会返回与之关联的值。如果键不存在,该方法就会对该键运用指定的函数对象算出一个值,将这个值与键关联起来,并返回计算得到的值。computeIfAbsent方法简化了将多个值与每个键关联起来的映射实现。

// Tasteful use of streams enhances clarity and conciseness

public class Anagrams {

    public static void main(String[] args) throws IOException {

        Path dictionary = Paths.get(args[0]);
        int minGroupSize = Integer.parseInt(args[1]);

        try (Stream<String> words = Files.lines(dictionary)) {

             words.collect(groupingBy(word -> alphabetize(word)))
            .values().stream()
            .filter(group -> group.size() >= minGroupSize)
            .forEach(g -> System.out.println(g.size() + ": " + g));

        }
    } // alphabetize method is the same as in original version
}

       即使你之前没怎么接触过Stream ,这段程序也不难理解。它在try-with-resources 块中打开词典文件,获得一个包含了文件中所有代码的Stream 。Stream 变量命名为words,是建议S tream 中的每个元素均为单词。这个Stream 中的pipe line 没有中间操作;它的终止操作将所有的单词集合到一个映射中,按照它们的字母排序形式对单词进行分组(详见第46 条) 。这个映射与前面两个版本中的是完全相同的。随后,在映射的values ()视图中打开了一个新Stream<List<String >> 。当然,这个Stream 中的元素都是换位词分组。Stream 进行了过滤,把所有分组大小小于minGroupSize 的单词都去掉了,最后,通过终止操作的forEach 打印出剩下的分组。

      在没有显式类型的情况下,仔细命名Lambda参数, 这对于Stream pipeline的可读性至关重要。

        Java 不支持基本类型的char Stream,最好避免利用Stream 来处理char值。

      如本条目中的范例程序所示, Stream pipeline 利用函数对象(一般是Lambda 或者方法引用)来描述重复的计算,而迭代版代码则利用代码块来描述重复的计算。

下列工作只能通过代码块,而不能通过函数对象来完成:

  • 从代码块中,可以读取或者修改范围内的任意局部变量;从Lambda 则只能读取final 或者有效的final 变量[ JLS 4.12.4 ],并且不能修改任何local 变量。
  • 从代码块中,可以从外国方法中return 、break 或continue 外围循环,或者抛出该方法声明要抛出的任何受检异常;从Lambda 中则完全无法完成这些事情。

stream可以使得完成下列这些工作变得易如反掌:

  • 统一转换元素的序列
  • 过滤元素的序列
  • 利用单个操作(如添加、连接或者计算其最小值)合并元素的顺序
  • 将元素的序列存放到一个集合中,比如根据某些公共属性进行分组
  • 搜索满足某些条件的元素的序列

总之,Stream 和 迭代代码块选择合适的用,不知选哪个时,可以两个都试试。

46 条:优先选择Stream 中无副作用的函数

例:构建一张表格,显示这些单词在一个文本文件中出现的频率:

// Uses the streams API but not the paradigm--Don't do this!

Map<String, Long> freq = new HashMap<>();

try (Stream<String> words = new Scanner(file).tokens()) {

    words.forEach(word -> {
        freq.merge(word.toLowerCase(), 1L, Long::sum);
    });

}

       使用了Stream 、Lambda 和方法引用,并且得出了正确的答案。简而言之,这根本不是Stream 代码;只不过是伪装成Stream 代码的迭代式代码。这段代码利用一个改变外部状态(频率表)的Lambda ,完成了在终止操作的forEach 中的所有工作。forEach 操作的任务不只展示由Stream 执行的计算结果,这在代码中并非好事,改变状态的Lambda 也是如此。

// Proper use of streams to initialize a frequency table

Map<String, Long> freq;

try (Stream<String> words = new Scanner(file).tokens()) {
    freq = words.collect(groupingBy(String::toLowerCase, counting()));
}

这个代码片段的作用与前一个例子一样,只是正确使用了Stream API ,变得更加简洁、清晰。

        Stream终止操作forEach 应该只用于报告St ream 计算的结果,而不是执行计算。有时候,也可以将forEach 用于其他目的,比如将Stream 计算的结果添加到之前已经存在的集合中去。

       将Stream 的元素集中到一个真正的Collection 里去的收集器比较简单。有三个这样的收集器: toList ()、toSet ()和toCollection(collectionFactory ) 。它们分别返回一个列表、一个集合和程序员指定的集合类型。

        总而言之,编写Stream pipeline 的本质是无副作用的函数对象。这适用于传入Stream及相关对象的所有函数对象。终止操作中的forEach 应该只用来报告由Stream 执行的计算结果,而不是让它执行计算。为了正确地使用Stream ,必须了解收集器。最重要的收集器工厂是toList 、toSet 、toMap 、groupingBy 和joining 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值