Effective Java读书笔记
第二章:创建和销毁对象
这一章主要讨论了对象的创建和销毁,包括如何使用静态工厂方法、避免创建不必要的对象、及时释放对象等。
本章的核心思想是:对于创建和销毁对象,要遵循一些基本的规则,例如使用静态工厂方法而不是直接使用构造方法、避免创建不必要的对象、及时销毁对象等。
使用静态工厂方法而不是直接使用构造方法有以下几个原因:
静态工厂方法可以有更具描述性的方法名,可以帮助提高代码的可读性和可维护性。
静态工厂方法可以控制对象的创建,例如可以通过缓存已经创建的对象来避免重复创建,从而提高性能。
静态工厂方法可以返回子类的对象,而构造方法不行。这可以为接口和抽象类提供方便的实现。
同时还要注意:
避免创建不必要的对象。例如,使用静态工厂方法创建单例模式对象,避免重复创建相同的对象。
及时销毁对象。例如,避免保持过多的对象引用,以免占用过多的内存空间。
使用try-with-resources语句关闭资源。例如,使用try-with-resources语句来关闭文件、网络连接等资源,避免资源泄漏和性能问题。
代码示例:
// 使用静态工厂方法创建对象
public class MyClass {
private final String name;
private final int age;
private MyClass(String name, int age) {
this.name = name;
this.age = age;
}
public static MyClass create(String name, int age) {
return new MyClass(name, age);
}
}
// 避免创建不必要的对象
String s = "hello";
// 不要这样做
String t = new String(s);
// 应该这样做
String u = s;
// 及时释放对象
MyClass myObj = new MyClass("John", 30);
// 使用 myObj
myObj = null; // 及时释放对象
第三章:对于所有对象都通用的方法
第三章的核心内容是关于对于类和接口的组合的最佳实践和设计原则。本章提供了一些最佳实践和设计原则,帮助Java程序员正确地设计类和接口的层次结构,以避免一些常见的问题,例如类和接口的耦合度过高、代码难以维护等。
本章的核心思想是:对于类和接口的组合,要遵循一些基本的规则,例如使用继承和实现来表达类和接口之间的关系、避免在类层次结构中过多的层次、通过抽象类和接口来定义类型等。下面我们将详细解释这些规则及其背后的原因。
以下是本章的一些核心内容:
使用继承和实现来表达类和接口之间的关系。继承表示一个类是另一个类的一种类型,而实现表示一个类实现了某个接口的全部方法。
避免在类层次结构中过多的层次。如果类层次结构太深,代码的可读性和可维护性会变差。应该尽量保持层次结构的扁平化。
优先使用组合而不是继承。组合是一种更加灵活的方式,可以在运行时动态地组合对象,而继承则是静态的,必须在编译时确定。
通过抽象类和接口来定义类型。抽象类和接口可以定义一组方法,从而定义一种类型,可以让我们更加灵活地组织代码,同时提高代码的可读性和可维护性。
代码示例:
// 重写equals()和hashCode()方法
public class Person {
private String name;
private int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
// 重写toString()方法
public class Person {
private String name;
private int age;
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
第四章:类和接口
第四章的核心内容是关于通用的Java编程规范和惯用法的介绍。这些规范和惯用法涵盖了Java编程中的许多方面,例如类和接口、方法、变量、异常、注释、并发等。这些规范和惯用法的目的是为了编写清晰、易于理解和维护的Java代码,同时提高代码的可读性、可靠性和性能。
以下是本章的一些核心内容:
类和接口:要使类和接口的设计尽可能简单、清晰、易于理解和使用。应该使用最少的方法和变量来完成一个特定的任务,并且尽量避免公开内部状态。
方法:方法应该尽量短小精悍、清晰易懂。应该使用简洁的参数列表、清晰的命名和注释、以及明确的返回类型和异常处理方式。
变量:变量的命名应该尽可能清晰、简洁和有意义,同时避免过度使用缩写和不必要的缩写。应该尽量使用final修饰符来使变量不可变,从而提高代码的可靠性和性能。
异常:应该尽量使用Java标准异常,而不是自定义异常。在捕获异常时,应该遵循最小化捕获原则,只捕获必要的异常,避免捕获所有异常。
注释:注释应该简洁明了、清晰易懂。注释的目的是解释代码的意图和实现细节,以帮助其他开发人员理解和维护代码。
并发:在编写多线程程序时,应该遵循一些最佳实践和惯用法,例如使用线程安全的类、避免共享可变状态、使用volatile和synchronized关键字来保护共享状态等。
代码示例:
// 尽量使用接口而不是抽象类
public interface Animal {
void makeSound();
}
// 避免使用非final的类
public final class MyFinalClass {
// 类的实现
}
第五章:泛型
泛型是Java语言中的一项重要特性,可以让我们编写更加通用、类型安全的代码。本章提供了一些最佳实践和设计原则,帮助Java程序员正确地使用泛型,以避免一些常见的问题,例如类型转换异常、编译时错误等。
-
在使用泛型时,要尽量使用通配符类型。通配符类型可以让我们编写更加通用的代码,同时保证类型安全。
-
在使用泛型时,要尽量避免使用原生态类型。原生态类型可以绕过编译时类型检查,容易引起类型转换异常。
-
在使用泛型时,要尽量使用类型参数。类型参数可以让我们编写更加通用的代码,同时保证类型安全。
-
在使用泛型时,要尽量避免在公共API中使用原始类型。原始类型会使代码变得不够健壮,同时也会使代码的可读性和可维护性变差。
-
在使用泛型时,要注意类型擦除。类型擦除是指在编译时泛型类型被擦除成原始类型,这可能会导致一些问题,例如类型转换异常、泛型数组创建异常等。
代码示例:
// 使用有限制通配符来增加API的灵活性
public class MyList<E> {
private List<E> list = new ArrayList<>();
public void addAll(Collection<? extends E> c) {
list.addAll(c);
}
}
// 优先考虑类型安全的泛型方法
public class Utils {
public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp) {
if (coll.isEmpty()) {
throw new IllegalArgumentException("Collection is empty");
}
T result = null;
for (T elem : coll) {
if (result == null || comp.compare(elem, result) > 0) {
result = elem;
}
}
return result;
}
}
第六章:枚举和注解
第六章的核心内容是关于枚举类型的最佳实践和设计原则。枚举类型是Java语言中的一项重要特性,可以让我们编写更加可读性强、类型安全的代码。本章提供了一些最佳实践和设计原则,帮助我们正确地使用枚举类型,以避免一些常见的问题,例如空指针异常、类型转换异常等。
以下是本章的一些核心内容:
-
使用枚举类型可以使代码更加清晰、易于理解。枚举类型可以将常量值和方法封装在一起,从而提高代码的可读性和可维护性。
-
枚举类型可以用来实现单例模式。枚举类型的单例模式可以避免线程安全问题,同时也可以避免反射攻击。
-
枚举类型可以用来实现有限状态机。有限状态机是一种重要的编程模型,可以用来描述复杂的系统行为,从而提高代码的可读性和可维护性。
-
在使用枚举类型时,要注意空指针异常。枚举类型中的每个枚举值都是一个对象,因此可能会出现空指针异常。
在使用枚举类型时,要注意类型转换异常。枚举类型中的每个枚举值都有一个唯一的名称和序号,因此可能会出现类型转换异常。
代码示例:
// 使用枚举来替代常量
public enum Size {
SMALL,
MEDIUM,
LARGE
}
// 在枚举类型中添加新方法
public enum Operation {
PLUS {
@Override
public double apply(double x, double y) {
return x + y;
}
},
MINUS {
@Override
public double apply(double x, double y) {
return x - y;
}
},
TIMES {
@Override
public double apply(double x, double y) {
return x * y;
}
},
DIVIDE {
@Override
public double apply(double x, double y) {
return x / y;
}
};
public abstract double apply(double x, double y);
}
第七章:方法
这一章主要介绍了方法的使用和设计原则,包括尽量将方法设计成静态方法、避免方法过长等。
代码示例:
// 尽量将方法设计成静态方法
public class MyUtils {
public static int max(int a, int b) {
return a > b ? a : b;
}
}
// 避免方法过长
public class MyCalculator {
public int calculate(int a, int b, int c, int d, int e) {
int result = 0;
// 计算过程
return result;
}
}
第八章:通用程序设计
第八章主要介绍通用程序设计的原则和技巧。通用程序设计是指编写可以用于多种应用场景的程序代码,也是高质量、可重用和易维护代码的重要基础。本章主要介绍了以下内容:
-
遵循标准的编程约定和风格。编程约定和风格可以帮助我们编写易读、易懂和易于维护的代码。
-
设计并遵循API规范。API规范是指编写可供其他开发人员使用的接口,遵循API规范可以使我们的代码更加通用、可重用。
-
使用接口和抽象类编写通用代码。接口和抽象类是通用程序设计的重要工具,可以使代码更加灵活和可扩展。
-
编写可重用的泛型代码。泛型代码可以使我们编写通用、类型安全和可重用的代码。
-
将对象的创建和使用分离。将对象的创建和使用分离可以使代码更加灵活、可扩展和可重用。
-
使用枚举类型编写类型安全的代码。枚举类型是一种类型安全、可重用和易维护的代码实现方式。
代码示例:
// 保护程序健壮性
public class MyCalculator {
public int divide(int a, int b) {
if (b == 0) {
throw new ArithmeticException("Division by zero");
}
return a / b;
}
}
// 使程序易于理解和修改
public class MyUtils {
public static String formatDate(Date date) {
// 日期格式化过程
return formattedDate;
}
}
第九章:异常
这一章主要介绍了异常的使用和设计原则,包括使用检查异常、不要忽略异常、不要过度捕获异常等。
代码示例:
// 使用检查异常
public class MyCalculator {
public int divide(int a, int b) throws IllegalArgumentException {
if (b == 0) {
throw new IllegalArgumentException("Division by zero");
}
return a / b;
}
}
// 不要忽略异常
public class MyFileUtils {
public static void copyFile(File source, File dest) throws IOException {
try (InputStream in = new FileInputStream(source);
OutputStream out = new FileOutputStream(dest)) {
// 文件复制过程
}
}
}
// 不要过度捕获异常
public class MyCalculator {
public int divide(int a, int b) throws IllegalArgumentException {
try {
return a / b;
} catch (ArithmeticException e) {
throw new IllegalArgumentException("Division by zero", e);
}
}
}
第十章:并发
第十章的核心内容是关于并发编程的最佳实践和设计原则。并发编程是现代软件开发中一个非常重要的领域,但也是一个充满挑战的领域。在并发编程中,多个线程同时访问共享资源,可能会引起一系列问题,例如竞态条件、死锁、饥饿等。因此,编写正确、高效、健壮的并发代码是一项非常重要的任务。
以下是本章的一些核心内容:
使用同步机制来保护共享资源。同步机制包括使用synchronized关键字、Lock接口、Atomic类等。使用同步机制可以保证共享资源的正确性和一致性。
避免过度同步。过度同步会降低并发性能,可能会引起死锁等问题。在使用同步机制时,要尽量减小同步的范围,以提高并发性能。
熟悉并发库。Java提供了丰富的并发库,包括Executor框架、并发集合、原子变量等。熟悉并发库可以帮助我们编写更加高效、健壮的并发代码。
理解线程安全性。线程安全性是指在多线程环境下,共享资源的行为符合预期。了解不同类型的线程安全性,例如不可变、线程安全、有条件线程安全、线程不安全等,可以帮助我们正确地设计并发代码。
避免死锁和饥饿。死锁和饥饿是并发编程中常见的问题,可以通过一些技术手段来避免。例如,避免嵌套锁、避免锁顺序死锁等。
代码示例:
// 使用同步代码块来确保线程安全
public class MyCounter {
private int count = 0;
public void increment() {
synchronized (this) {
count++;
}
}
public int getCount() {
synchronized (this) {
return count;
}
}
}
// 使用并发集合来优化并发性能
public class MyConcurrentMap {
private ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
public void put(String key, String value) {
map.put(key, value);
}
public String get(String key) {
return map.get(key);
}
}