Effective Java 3rd (五)

博客围绕Java编程给出多项建议,包括使用标记接口定义类型、lambda表达式优于匿名类、方法引用优于lambda表达式等。还提及Stream的使用、参数有效性检查、防御性拷贝等内容,为Java开发者提供了实用的编程指导。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

  1. 使用标记接口定义类型
  2. lambda表达式优于匿名类
  3. 方法引用优于lambda表达式
  4. 优先使用标准的函数式接口
  5. 明智审慎地使用Stream
  6. 优先考虑流中无副作用的函数
  7. 优先使用Collection而不是Stream来作为方法的返回类型
  8. 谨慎使用流并行
  9. 检查参数有效性
  10. 必要时进行防御性拷贝

41. 使用标记接口定义类型

标记接口(marker interface),不包含方法声明,只是指定(或“标记”)一个类实现了具有某些属性的接口。 例如,考虑 Serializable 接口。
标记注解,定义一个注解标记类,方法等

标记接口优点:
1、标记接口可以定义出一个接口类型实例,在编译期可以捕获错误,而标记注解则不可以。
2、标记接口对于标记注解的另一个优点是可以更精确地定位目标。
例如Collection的子接口Set,扩展了Collection的几个方法,但是他已经被限定在Set这个接口内,不会超出Collection接口的范围。

总之,标记接口和标记注释都有其用处。 如果你想定义一个没有任何关联的新方法的类型,一个标记接口是一种可行的方法。 如果要标记除类和接口以外的程序元素,或者将标记符合到已经大量使用注解类型的框架中,那么标记注解是正确的选择。

42. lambda 表达式优于匿名类

使用单一抽象方法的接口(或者很少使用抽象类)被用作函数类型。,jdk8以前一般用匿名内部类,jdk8以后可选择用lambda表达式。

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

用lambda

Collections.sort(words,(s1, s2) -> Integer.compare(s1.length(), s2.length()));

使用方法引用可以进一步简化

//comparingInt 是Comparator中的方法,此处用静态导入
Collections.sort(words, comparingInt(String::length));

利用"words"中List的sort方法进一步简化

words.sort(comparingInt(String::length));

再看一个之前枚举类的例子,操作符实现加减乘除

// Enum type with constant-specific class bodies & 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);
}

优化为

public enum Operation {
PLUS ("+", (x, y) -> x + y),
MINUS ("-", (x, y) -> x - y),
TIMES ("*", (x, y) -> x * y),
DIVIDE("/", (x, y) -> x / y);

private final String symbol;
private final DoubleBinaryOperator op;

Operation(String symbol, DoubleBinaryOperator op) {
	this.symbol = symbol;
	this.op = op;
} 
@Override
public String toString() { return symbol; }
public double apply(double x, double y) {
	return op.applyAsDouble(x, y);
}
}

优化后代码更简洁,其中使用到了java自带的一个类DoubleBinaryOperator。

总结:
1、当lambda表达式中的代码行数太长(建议最多3行)时可使用。
2、lambda中无法使用this。
3、除非必须创建非函数式接口类型的实例,否则不要使用匿名类作为函数对象。

43. 方法引用优于 lambda 表达式

方法引用语法

方法引用类型举例lamda等式
StaticInteger::parseIntstr -> Integer.parseInt(str)
BoundInstant.now()::isAxerInstant then = Instant.now();t -> then.isAxer(t)
UnboundString::toLowerCasestr -> str.toLowerCase()
Class ConstructorTreeMap<K, V>::new() -> new TreeMap<K, V>
Array Constructorint[]::newlen -> new int[len]

使用方法引用的原因:
比lambda更简洁,但是我个人觉得,在不熟悉api的情况下,可读性从高到低是:
匿名内部类>lambda>方法引用

44. 优先使用标准的函数式接口

优先使用java提供的函数式接口,可以降低学习成本。当无法满足需求时才重新编写自己的接口。
在 java.util.Function 中有 43 个接口。不能指望全部记住它们,但是如果记住了六个基本接口,就可以在需要它们时派生出其余的接口。基本接口操作于对象引用类型。 Operator 接口表示方法的结果和参数类型相同。Predicate 接口表示其方法接受一个参数并返回一个布尔值。 Function 接口表示方法其参数和返回类型不同。 Supplier 接口表示一个不接受参数和返回值 (或“供应”) 的方法。最后, Consumer 表示该方法接受一个参数而不返回任何东西,本质上就是使用它的参数。六种基本函数式接口概述如下:

接口方法示例
UnaryOperatorT apply(T t)String::toLowerCase
BinaryOperatorT apply(T t1, T t2)BigInteger::add
Predicateboolean test(T t)Collection::isEmpty
Function<T,R>R apply(T t)Arrays::asList
SupplierT get()Instant::now
Consumervoid accept(T t)System.out::println

45. 明智审慎地使用 Stream

流有以下特性:
1、流管道由源流(source stream)的零或多个中间操作和一个终结操作组成。
2、管道延迟(lazily)计算求值:计算直到终结操作被调用后才开始,而为了完成终结操作而不需要的数据元素永远不会被计算出来。
3、Stream API 流式的(fluent)::它设计允许所有组成管道的调用被链接到一个表达式中。事实上,多个管道可以链接在一起形成一个表达式

java缺乏对原始字符流的支持,所以以下代码将打印出char对应的int。chars()返回的就是IntStream

"Hello world!".chars().forEach(System.out::print);

迭代代码块优点:
1、可以读取或修改范围内的任何局部变量;lambda不行。
2、可以从封闭方法返回,中断或继续封闭循环,或抛出声明此方法的任何已检查异常; 从一个
lambda 你不能做这些事情。

流优点:
1、统一转换元素序列
2、过滤元素序列
2、使用单个操作组合元素序列 (例如添加、连接或计算最小值)
4、将元素序列累积到一个集合中,可能通过一些公共属性将它们分组
5、在元素序列中搜索满足某些条件的元素

总结:
两种方式都可以,满足其中的优点就选哪种。当流太长时,维护跟可读性会变差

46. 优先考虑流中无副作用的函数

流的建议用法是:
将计算结构化为一系列转换(过滤,转换成其它对象流),得到最终结果。

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

这种用法更像是普通的迭代类,最终结果是lambda中的merge方法来实现,而没有用到流的优点。

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

这种用法则使代码更加易读并利用了流的转换。

Collectors中有三个的toMap方法,都是对toMap(keyMapper,valueMapper,mergeFunction,mapSupplier)的封装
1、toMap(keyMapper、valueMapper)分别映射到key跟value的转换,遇到重复的key抛出异常,返回的map实例是HashMap

// Using a toMap collector to make a map from string to enum
private static final Map<String, Operation> stringToEnum =
	Stream.of(values()).collect(toMap(Object::toString, e -> e));

2、toMap(keyMapper,valueMapper,mergeFunction)分别映射到key跟value的转换,并且可定义map的重复key处理策略,返回的map实例是HashMap。
如想定义key为艺术家,value为专辑名,按销售量从高到低的map,maxBy是BinaryOperator的静态导入方法。

// Collector to generate a map from key to chosen element for key
Map<Artist, Album> topHits = albums.collect(
	toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));

除了用BinaryOperator的重复策略之外,也可以自己定义

// Collector to impose last-write-wins policy
toMap(keyMapper, valueMapper, (oldVal, newVal) ->newVal)

3、toMap(keyMapper,valueMapper,mergeFunction,mapSupplier)在第二点中的toMap方法的基础上,可以选择创建的哪种map来创建实例

toMap(keyMapper, valueMapper, (oldVal, newVal) ->newVal,TreeMap::new)

47. 优先使用 Collection 而不是 Stream 来作为方法的 返回类型

Collection 接口是 Iterable 的子类型,并且具有 stream 方法,因此它提供迭代和流访问。 因此,
Collection 或适当的子类型通常是公共序列返回方法的最佳返回类型。

线性结构(List,Set,Collection),优先选择Collection作为返回类型,无法实现collection中的方法则考虑返回Iterable接口

流转换为迭代器接口

// Adapter from Stream<E> to Iterable<E>
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
	return stream::iterator;
}

迭代器转流

// Adapter from Iterable<E> to Stream<E>
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
	return StreamSupport.stream(iterable.spliterator(), false);
}

集合框架类AbstractList,AbstractSet等可以方便的编写自己的集合

48. 谨慎使用流并行

// Stream-based program to generate the first 20 Mersenne primes
public static void main(String[] args) {
primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE)).
filter(mersenne -> 		mersenne.isProbablePrime(50)).limit(20)
.forEach(System.out::println);
} 

static Stream<BigInteger> primes() {
	//并行流
	//return Stream.iterate(TWO, BigInteger::nextProbablePrime).parallel();
	return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}

这段代码,如果改成并行流,会花费很多时间,不输出任何内容。
原因是这个流是无线流(1…无限),并行流无法进行分割处理。
**通常,并行性带来的性能收益在 ArrayList 、 HashMap 、 HashSet 和 ConcurrentHashMap 实例数组、 int 类型范围和 long 类型的范围的流上最好。**因为它们中的元素是有限的。
并行流的foreach循环,输出的不是顺序的,如果需要顺序,需要用forEachOrdered

一个使用并行流的例子

// 作为并行性有效的流管道的简单示例,请考虑此函数来计算π(n),素数小于或等于 n:
// Prime-counting stream pipeline - benefits from parallelization
static long pi(long n) {
return LongStream.rangeClosed(2, n)
.mapToObj(BigInteger::valueOf)
.filter(i -> i.isProbablePrime(50))
.count();
}

上面这个代码,改成并行流后能提高性能,在我的机器用10000000 计算,普通流21秒,并行流只要5秒。
总结:
使用并行流要做好测试,保证确实能提高性能。

49. 检查参数有效性

Objects.requireNonNull 判断非空
注释中用@throws来表示调用方法可能抛出的异常。

/**
* Returns a BigInteger whose value is (this mod m). This method
* differs from the remainder method in that it always returns a
* non-negative BigInteger.
* *
@param m the modulus, which must be positive
* @return this mod m
* @throws ArithmeticException if m is less than or equal to 0
*/
public BigInteger mod(BigInteger m) {
if (m.signum() <= 0)
	throw new ArithmeticException("Modulus <= 0: " + m);
	... // Do the computation
}

应该设计一些方法,使参数检查更高效。每次编写方法或构造方法时,都应该考虑对其参数存在哪些限制。

50. 必要时进行防御性拷贝

类中有可变的变量时需要考虑防御性拷贝。

// Broken "immutable" time period class
public final class Period {
	private final Date start;
	private final Date end;
/**
* @param start the beginning of the period
* @param end the end of the period; must not precede start
* @throws IllegalArgumentException if start is after end
* @throws NullPointerException if start or end is null
*/
public Period(Date start, Date end) {
	if (start.compareTo(end) > 0)
		throw new IllegalArgumentException(
	start + " after " + end);
	this.start = start;
	this.end = end;
} 
public Date start() {
	return start;
} 
public Date end() {
	return end;
} .
.. // Remainder omitted
}

客户端可以通过先传值再改值的方式攻击

// Attack the internals of a Period instance
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // Modifies internals of p!
// Repaired constructor - makes defensive copies of parameters
public Period(Date start, Date end) {
	this.start = new Date(start.getTime());
	this.end = new Date(end.getTime());
	if (this.start.compareTo(this.end) > 0)
		throw new IllegalArgumentException(
	this.start + " after " + this.end);
}

注意:拷贝需要在参数检查之前,因为在检查参数和拷贝参数之间的漏洞窗口期间保护类不受其他线程对参数的更改的影响。还需要注意没有用clone方法,因为Date类可以子类化。

当用以下代码,又可以进行攻击:

// Second attack on the internals of a Period instance
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78); // Modifies internals of p!

所以需要再次修改:

// Repaired accessors - make defensive copies of internal fields
public Date start() {
	return new Date(start.getTime());
} 
public Date end() {
	return new Date(end.getTime());
}

总之,如果一个类有从它的客户端获取或返回的可变组件,那么这个类必须防御性地拷贝这些组件。如果拷贝的成本太高,并且类信任它的客户端不会不适当地修改组件,则可以用文档替换防御性拷贝。此例子中的Date类可以考虑用Instant或者LocalDateTime,因为这两个类是不可变类。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值