写给大忙人的JavaSE8书后习题简析-第一章

lambda表达式

第一题

Arrays.sort方法中的比较器代码的线程与调用sort的线程是同一个吗?

是的,看下源码就知道了。
Arrays.sort默认采用的是TimSort的方法(即传统Mergesort的优化版本)。当用户指定了一个Comparator时,他会同步的回调这个Comparator的compare方法来作为比较的参照,因此显然这里并不存在多线程的问题。
事实上在JDK1.8以后就提供了并行的排序方法Arrays.parallelSort。
不过需要注意的是,经过测验,在小数量集上并行排序的速度反倒不如非并行的快,这主要受数量级大小以及并行的核心数的影响。在通常的4核PC机中,这个转折点大约在1e6这个数量级左右。

第二题

使用java.io.File类的listFiles(FileFilter)和isDirectory方法,编写一个返回指定目录下所有子目录的方法。使用lambda表达式来代替FileFilter对象,再将它改写为一个方法的引用。

这道题主要就是考察lambda的基本用法,以及于其他方法的对比。

import java.io.File;
import java.io.FileFilter;

public class Task2 {
	public static void main(String[] args) {
		File file=new File("/home/myths");
		//File[] files=file.listFiles((f)->f.isDirectory());
		//File[] files=file.listFiles(File::isDirectory);
		File[] files=file.listFiles(new FileFilter() {

			@Override
			public boolean accept(File pathname) {
				return pathname.isDirectory();
			}
		});
		for(File f : files){
			System.out.println(f.getAbsolutePath());
		}
	}
}

第三题

使用java.io.File类的list(FilenameFilter)方法,编写一个返回指定目录下、具有指定扩展名的所有文件。使用lambda表达式(而不是FilenameFilter)来实现。他会捕获闭合作用域中的哪些变量?

这道题主要考察捕获外部变量。

import java.io.File;

public class Task3 {
	public static void main(String[] args){
		File file=new File("/home/myths");
		String suffix=".txt";
		String[] files=file.list((File dir,String name)-> name.endsWith(suffix));
		for(String f:files){
			System.out.println(f);
		}
	}
}

捕获了suffix变量。

第四题

对于一个指定的File对象数组,首先按照路径的目录排序,然后对每组目录中的元素再按照路径名排序。请使用lambda表达式(而不是Comparator)来实现。

这道题翻译的很差劲,我特地找了下英文原版的题目,发现完全是两个问题。。。原版题目如下:

Given an array of File objects, sort it so that the directories come before the files, and within each group, elements are sorted by path name. Use a lambda expression, not a Comparator.

这样就很清楚了。

import java.io.File;
import java.util.Arrays;

public class Task4 {
    public static void main(String[] args) {
        File[] files = new File("/home/myths/").listFiles();
        Arrays.sort(files, (first, second) -> {
            if (first.isDirectory() && second.isDirectory() || first.isFile() && second.isFile()) {
                return first.getPath().compareTo(second.getPath());
            } else {
                if (first.isDirectory())
                    return -1;
                else
                    return 1;
            }
        });
        for (File file : files) {
            System.out.println(file.getAbsolutePath());
        }
    }
}

第五题

从你的项目中选取一个包含一些ActionListener、Runnable或者其他类似代码的文件。将他们替换为lambda表达式。这样能节省多少行代码?替换后的代码是否有更好的可读性?在这个过程中你使用了方法引用吗?

没有类似的项目,不过显然能节省不少的代码,可读性也会有所提高。如果使用了方法引用,那么可读性和简洁性会进一步提高。

第六题

你是否讨厌在Runnable实现中处理检查器异常?编写一个捕获所有异常的uncheck方法,再将它改造为不需要检查异常的方法。例如:

    new Thread(uncheck(
        () -> {
            System.out.println("Zzz");
            Thread.sleep(1000);
        })).start();
    //看,不需要catch(InterruptedException);

主要考察@FunctionalInterface接口的用法,利用这个接口可以很方便的对函数类型的变量进行封装。

public class Task7 {

    public @FunctionalInterface
    interface RunnableEx {
        void run() throws Exception;
    }

    public static Runnable uncheck(RunnableEx runner) {
        return () -> {
            try {
                runner.run();
            } catch (Exception ex) {
                System.err.println(ex);
            }
        };
    }

    public static void main(String[] args) {
        new Thread(uncheck(
                () -> {
                    System.out.println("Zzz");
                    Thread.sleep(1000);
                })
        ).start();
    }
}

第七题

编写一个静态方法andThen,它接受两个Runnable实例作为参数,并返回一个分别运行这两个实例的Runnable对象。在main方法中,向andThen方法传递两个lambda表达式,并运行返回的实例。

巩固函数接口的使用。

public class Task7 {
    public static Runnable andThen(Runnable runner1, Runnable runner2) {
        return () -> {
            runner1.run();
            runner2.run();
        };
    }

    public static void main(String[] args) {
        new Thread(andThen(
                () -> System.out.println("Runner 1"),
                () -> System.out.println("Runner 2")
        )).start();
    }
}

第八题

当一个lambda表达式捕获了如下增强for循环中的值时,会发生什么?这样做是否合法?每个lambda表达式都捕获了一个不同的值,还是他们都获得了最终的值?如果使用传统的for循环,例如for (int i=0;i<names.length;i++),又会发生什么?

String[] names = { "Peter", "Paul", "Mary" };
List<Runnable> runners = new ArrayList<>();
for (String name : names)
    runners.add(() -> System.out.println(name));

考察lambda的变量捕获,这里如果采用上面的增强for循环是不会有问题的:

public class Task8 {

    public static void main(String[] args) {
        String[] names = {"Peter", "Paul", "Mary"};
        List<Runnable> runners = new ArrayList<>();

//      增强for循环
        for (String name : names)
            runners.add(() -> System.out.println(name));
        for (Runnable runnable : runners) {
            runnable.run();
        }

//        传统for循环
//        for (int i = 0; i < names.length; i++)
//            runners.add(() -> System.out.println(names[i]));
//        for (Runnable runnable : runners) {
//            runnable.run();
//        }
    }
}

但是,这里是不允许使用传统for循环的,否则他会报如下错误:

Error:(16, 56) java: local variables referenced from a lambda expression must be final or effectively final

显然,lambda表达式里取到的这个i由于发生了变化,不再是一个“有效的final值”,不满足lambda“被引用变量是不能修改的”这一规范。

第九题

编写一个继承Collection接口的子接口Collection2,并添加一个默认方法void forEachIf(Consumer<T> action, Predicate<T> filter),用来将action应用到所有filter返回true的元素上。你能够如何使用它?

这里主要考察一些常见的函数式接口以及default函数声明。

import java.util.Collection;
import java.util.function.Consumer;
import java.util.function.Predicate;

public class Task9 {
    public interface Collection2<T> extends Collection<T> {

        default void forEachIf(Consumer<T> action, Predicate<T> filter) {
            forEach(item -> {
                filter.test(item) ? action.accept(item);
            });
        }
    }
}

第十题

浏览Collections类中的方法。如果哪一天你可以做主,你会将每个方法放到哪个接口中?这个方法会是一个默认方法还是静态方法?

不是很清楚其中的道理,不敢瞎说。。。

第十一题

假如你有一个实现了两个接口I和J的类,这两个接口都有一个void f()方法。如果I接口中的f方法是一个抽象的、默认或者静态方法,并且J接口中的f方法是也一个抽象的、默认或者静态方法,分别会发生什么?如果这个类继承自S类并实现了接口I,并且S和I中都有一个void f()方法,又会分别发生什么?

这两个接口之间的搭配有点复杂,不过经过测试,我总结了下面的几个规则:

  1. 只要有一个接口中是抽象函数,那么这个类必须要重载这个函数重新实现。
  2. 如果一个是静态函数一个是默认函数,那么,最终显示出来的是默认函数的特性。

至于既有extends又有implements的情况。。。有点麻烦,也就不一个一个测了,到时候用到再测吧。。。

第十二题

在过去,你知道向接口中添加方法是一种不好的形式,因为他会破坏已有的代码。现在你知道了可以像接口中添加新方法,同时能够提供一个默认的实现。这样做安全程度如何?描述一个Collection接口的新stream方法会导致遗留代码编译失败的场景。二进制的兼容性如何?JAR文件中的遗留代码是否还能运行?

所谓安全问题大概就是指对于旧的版本,忽然多出一个可以执行却没有啥作用的函数,略微违背了"封装隐藏"的思想。但是在旧的版本中,他们并不知道这个函数的存在,所以我觉得一般情况下也不存在什么安全问题吧。
第二问不是很清楚。。。

参考资料

Java SE 8 for the Really Impatient
Answers found in github