网站规划包括哪些内容,建站城,织梦做社交网站合适吗,做网站彩票的代理好吗Effective Java 第一章 引言第二章 创建和销毁对象第1条#xff1a;用静态工厂方法代替构造器第2条#xff1a;遇到多个构造器参数时要考虑使用构建器第3条#xff1a;用私有构造器或者枚举类型强化Singletion属性第4条#xff1a;通过私有构造器强化不可实例化的能力第5条… Effective Java 第一章 引言第二章 创建和销毁对象第1条用静态工厂方法代替构造器第2条遇到多个构造器参数时要考虑使用构建器第3条用私有构造器或者枚举类型强化Singletion属性第4条通过私有构造器强化不可实例化的能力第5条优先考虑依赖注入来引用资源第6条避免创建不必要的对象第七条消除过期的对象引用第8条避免使用终结方法和清除方法第9条try-with-resources优先于try-finally 第三章 对于所有对象都通用的方法第10条覆盖equals时请遵守通用约定第11条覆盖 equals 时总要覆盖 hashCode第12条始终要覆盖 toString第13条谨慎地覆盖clone第14条考虑实现Comparable接口 第四章 类和接口第15条使类和成员的可访问性最小化第16条要在公有类中使用访问方法而非公有域第17条使可变性最小化第18条复合优先于继承第19条要么设计继承并提供文档说明要么禁止继承第20条接口优于抽象类第21条为后代设计接口第22条接口只用于定义类型第23条类层次优于标签类第24条静态成员类由于非静态成员类第25条限制源文件为单个顶级类 第五章 泛型第26条请不要使用原生态类型第27条消除非受检的警告第28条列表优于数组第29条优先考虑泛型第30条优先考虑泛型方法第31条利用有限制通配符来提升API的灵活性第32条谨慎并用泛型和可变参数第33条优先考虑类型安全的异构容器 第六章 枚举和注解第34条用enum代替int常量第35条用实例域代替序数第36条用EnumSet代替位域第37条用EnumMap代替序数索引第38条用接口模拟可扩展的枚举第39条注解优先于命名模式第40条坚持使用Override注解第41条用标记接口定义类型 第七章 Lambda 和 Stream第42条Lambda优先于匿名类第43条方法引用优先于Lambda第44条坚持使用标准的函数接口第45条谨慎使用Stream第46条优先选择Stream中无副作用的函数第47条Stream要优先用Collection作为返回类型第48条谨慎使用Stream并行 第八章 方法第49条检查参数的有效性第50条必要时进行保护性拷贝第51条谨慎设计方法签名第52条慎用重载第53条慎用可变参数第54条返回零长度的数组或者集合、而不是null第55条谨慎返回optional第56条为所有导出的API元素编写文档注释 第九章 通用编程第57条将局部变量的作用域最小化第58条for-each循环优先于传统的for循环第59条了解和使用类库第60条如果需要精确的答案请避免使用float和double第61条基本类型优先于装箱基本类型第62条如果其他类型更适合则尽量避免使用字符串第63条了解字符串连接的性能第64条通过接口引用对象第65条接口优先于反射机制第66条谨慎地使用本地方法第67条谨慎地进行优化第68条遵守普遍接受的命名惯例 第十章 异常第69条只针对异常的情况才使用异常第70条对可恢复的情况使用受检异常对编程错误使用运行时异常第71条避免不必要地使用受检异常第72条优先使用标准的异常第73条抛出和抽象对应的异常第74条每个方法抛出的所有异常都要建立文档第75条在细节消息中包含失败-捕获信息第76条努力使失败保持原子性第77条不要忽略异常 第十一章 并发第78条同步访问共享的可变数据第79条避免过度同步第80条executor、task和stream优先于线程第81条并发工具优先于wait和notify第82条线程安全性的文档化第83条慎用延迟初始化第84条不要依赖于线程调度器 第十二章 序列化第85条其他方法优先于Java序列化第86条谨慎地实现Serializable接口第87条考虑使用自定义的序列化形式第88条保护性地编写readObject方法第89条对于实例控制枚举类型优先于readResolve第90条考虑用序列化代理代替序列化实例 第一章 引言
建议配合书本一起看
第二章 创建和销毁对象
本章的主题是创建和销毁对象何时以及如何创建对象何时以及如何避免创建对象如何确保它们能够适时地销毁以及如何管理对象销毁之前必须进行的各种清理动作。
第1条用静态工厂方法代替构造器
1、静态工厂方法与构造器不同的第一大优势在于它们有名称
public class Person {private String name;private int age;private Person(String name, int age) {this.name name;this.age age;}// 创建一个Person对象public static Person create(String name, int age) {return new Person(name, age);}
}// 使用静态工厂方法创建对象
Person person Person.create(John, 25);通过使用静态工厂方法我们可以清晰地知道我们正在创建一个Person对象。
2、静态工厂方法与构造器不同的第二大优势在于不必在每次调用它们的时候都创建一个新对象
这个就是我们所熟悉的单例模式可以避免创建不必要的重复对象
public class Singleton {private static final Singleton INSTANCE new Singleton();private Singleton() {// 私有构造器}public static Singleton getInstance() {return INSTANCE;}
}// 获取单例对象
Singleton singleton1 Singleton.getInstance();
Singleton singleton2 Singleton.getInstance();System.out.println(singleton1 singleton2); // true3、静态工厂方法与构造器不同的第三大优势在于它们可以返回原返回类型的任何子类型的对象
这样我们在选择返回对象的类时就有了更大的灵活
public interface Shape {void draw();
}public class Circle implements Shape {Overridepublic void draw() {System.out.println(Drawing a circle);}public static Circle create() {return new Circle();}
}public class Rectangle implements Shape {Overridepublic void draw() {System.out.println(Drawing a rectangle);}public static Rectangle create() {return new Rectangle();}
}// 使用静态工厂方法创建不同类型的对象
Shape circle Circle.create();
Shape rectangle Rectangle.create();circle.draw(); // Drawing a circle
rectangle.draw(); // Drawing a rectangle在上面的示例中Shape接口有两个实现类Circle和Rectangle它们都提供了一个名为create()的静态工厂方法来创建对象。通过使用静态工厂方法我们可以根据需要返回不同类型的对象而不仅仅是返回原返回类型。
通过使用静态工厂方法我们可以获得更好的可读性、灵活性和控制对象的创建过程。这些优势使得静态工厂方法成为替代构造器的一种常用模式。
4、静态工厂的第四大优势在于所返回的对象的类可以随着每次调用而发生改变这取决于静态工厂方法的参数值
这条与第三大优势一样只是多了参数根据参数返回不同类型
public interface Animal {void makeSound();
}public class Dog implements Animal {Overridepublic void makeSound() {System.out.println(Woof!);}public static Dog create() {return new Dog();}
}public class Cat implements Animal {Overridepublic void makeSound() {System.out.println(Meow!);}public static Cat create() {return new Cat();}
}public class AnimalFactory {public static Animal createAnimal(String type) {if (type.equalsIgnoreCase(dog)) {return Dog.create();} else if (type.equalsIgnoreCase(cat)) {return Cat.create();} else {throw new IllegalArgumentException(Invalid animal type);}}
}// 使用静态工厂方法创建不同类型的动物对象
Animal dog AnimalFactory.createAnimal(dog);
Animal cat AnimalFactory.createAnimal(cat);dog.makeSound(); // Woof!
cat.makeSound(); // Meow!5、静态工厂的第五大优势在于方法的返回的对象所属的类在编写包含该静态工厂方法的类时可以不存在
这条主要讲述通过静态工厂方法来创建对象从而提供更好的封装和控制。如下面创建对象之前可以进行一些验证和处理逻辑。
public class DatabaseConnection {private String url;private String username;private String password;private DatabaseConnection(String url, String username, String password) {this.url url;this.username username;this.password password;}public static DatabaseConnection createConnection(String url, String username, String password) {// 这里可以进行一些验证和处理逻辑return new DatabaseConnection(url, username, password);}public void connect() {// 连接数据库的逻辑System.out.println(Connected to database: url);}
}// 在其他类中使用静态工厂方法创建数据库连接对象
DatabaseConnection connection DatabaseConnection.createConnection(jdbc:mysql://localhost:3306/mydb, root, password);
connection.connect(); // Connected to database: jdbc:mysql://localhost:3306/mydb6、静态工厂方法的主要缺点在于类如果不含有公有的或者受保护的构造器就不能被子类化
就是不能继承但是这样也会因祸得福因为它鼓励程序员使用复合而不是继承
public class Parent {private Parent() {// 私有构造器}public static Parent create() {return new Parent();}
}public class Child extends Parent {// 编译错误无法从父类继承私有构造器
}7、静态工厂方法的第二个缺点在于程序员很难发现它们
在API文档中它们没有像构造器那样在API文档中明确标识出来所以较难发现但是可以通过在类或者接口注释中遵守标准的命名习惯也可以弥补这一劣势。
第2条遇到多个构造器参数时要考虑使用构建器
静态工厂和构造器有个共同的局限性它们都不能很好地扩展到大量的可选参数。
对于多个参数构造类时通常会使用重叠构造器或者javaBeans模式但是都有想应得缺点如下例(为了代码简单只写三个参数)
重叠构造器
public class User {private String id;private String name;private Integer age;// ...其他属性public User(String id) {this.id id;}public User(String id, String name) {this.id id;this.name name;}public User(String id, String name, Integer age) {this.id id;this.name name;this.age age;}// ...其他参数构造方法
}重叠构造器的缺点主要有以下几点 参数顺序容易混淆当有多个构造器时每个构造器接受的参数组合可能不同参数的顺序容易混淆导致使用者难以记住正确的参数顺序。 可读性差重叠构造器的参数列表可能会很长特别是在参数较多的情况下代码可读性较差难以理解和维护。 扩展性差如果需要添加新的参数或者修改参数的默认值需要修改所有的构造器并且可能需要修改调用构造器的代码这样会导致代码的维护成本增加。 容易出错由于参数顺序容易混淆使用重叠构造器创建对象时容易出错传入错误的参数导致对象状态不正确。
javaBeans模式
先调用一个无参构造器来创建对象然后再调用setter方法来设置每个必要得参数以及几个可选参数。
User user new User();
user.setId(11);
user.setName(22);这种模式弥补了重叠构造器得不足创建实例很容易代码也易读。遗憾得是
javaBeans模式自身有着很严重得缺点 对象可能处于不一致的状态在使用JavaBeans模式创建对象时对象可能处于不一致的状态。因为对象的构造过程被分成了多个步骤每个步骤只设置了部分属性可能会导致对象在某些属性上缺失值或者属性之间存在依赖关系从而导致对象的不一致状态。 对象的可变性使用JavaBeans模式创建的对象是可变的即可以通过调用setter方法来修改对象的属性。这种可变性可能会导致对象在多线程环境下出现并发问题需要额外的同步措施来保证对象的线程安全性。 缺乏强制性JavaBeans模式没有强制要求在构造过程中设置必要的属性因此可能会导致对象在创建后缺少必要的属性从而导致对象无法正常使用。 不可变性的缺失使用JavaBeans模式创建的对象是可变的无法保证对象的不可变性。不可变对象具有线程安全性、安全共享和更好的可靠性等优点但是JavaBeans模式无法提供这些优点。
所以这条建议的主要目的是解决在构造器参数较多时使用构造器创建对象可能会导致参数列表冗长、难以理解和容易出错的问题。使用构建器可以提供更好的可读性和易用性。
构建器 采用了建造者模式
public class Person {private final String name;private final int age;private final String address;// 其他属性...private Person(Builder builder) {this.name builder.name;this.age builder.age;this.address builder.address;// 其他属性...}public static class Builder {private final String name;private int age;private String address;// 其他属性...public Builder(String name) {this.name name;}public Builder age(int age) {this.age age;return this;}public Builder address(String address) {this.address address;return this;}// 其他方法...public Person build() {return new Person(this);}}
}// 使用构建器创建Person对象
Person person new Person.Builder(John).age(30).address(123 Main St)// 设置其他属性....build();构建器提供了一系列方法来设置对象的属性每个方法都返回构建器本身以实现链式调用。最后通过调用build()方法来创建一个完整的Person对象。
第3条用私有构造器或者枚举类型强化Singletion属性
在这条建议中主要讲了两种实现Singleton单例模式的方法使用私有构造器和使用枚举类型 来避免对象的重复创建保证类的唯一性
私有构造器
饿汉模式
public class Singleton {private static final Singleton INSTANCE new Singleton();private Singleton() {// 私有构造器}public static Singleton getInstance() {return INSTANCE;}
}懒汉模式
public class Singleton {private static Singleton instance;private Singleton() {// 私有构造器}public static Singleton getInstance() {if (instance null) {instance new Singleton();}return instance;}
}上方饿汉模式如果没有使用也已经建好了就造成资源 懒汉模式容易起到了Lazy Loading 的效果但是多线程下容易出现线程安全问题 并且上面两种方式都可以通过反射机制调用私有构造方法造成重复创建枚举模式可以防止这种行为
枚举
public enum Singleton {INSTANCE;// 枚举类型的字段和方法
}第4条通过私有构造器强化不可实例化的能力
这种技术通常被用于工具类或者包含静态方法和静态字段的类以防止被实例化。
public class UtilityClass {// 私有构造器防止类被实例化private UtilityClass() {throw new AssertionError(Utility class cannot be instantiated);}// 静态方法public static void doSomething() {// 执行某些操作}// 静态字段public static final int MAX_VALUE 100;
}在上面的示例中UtilityClass 是一个工具类它包含了一个私有构造器和一些静态方法和静态字段。私有构造器通过抛出 AssertionError 异常来防止类被实例化。这样其他类就无法通过 new UtilityClass() 来创建 UtilityClass 的实例。
通过这种方式我们可以确保 UtilityClass 只能被用作静态方法和静态字段的容器而不能被实例化也提供了更好的代码可读性和维护性。如果允许实例化就违反了设计意图可能导致误用和滥用。
第5条优先考虑依赖注入来引用资源
依赖注入是一种设计模式通过将依赖关系从代码中移除使得代码更加灵活、可测试和可维护。
这个是策略模式如果一个接口可能会有多个实现方式的话特别适合例如一个查询接口传不同参数查询的逻辑都不同且较多种的话会一直if else使用这个方式可以减少if else
public class UserService {private UserRepository userRepository;// 通过构造器注入依赖public UserService(UserRepository userRepository) {this.userRepository userRepository;}public void addUser(User user) {userRepository.save(user);}
}public interface UserRepository {void save(User user);
}public class UserRepositoryImpl1 implements UserRepository {public void save(User user) {// 第一个实现类处理逻辑}
}public class UserRepositoryImpl2 implements UserRepository {public void save(User user) {// 第二个实现类处理逻辑}
}public class Main {public static void main(String[] args) {UserRepository userRepository new UserRepositoryImpl1();UserService userService new UserService(userRepository);User user new User(John, Doe);userService.addUser(user);}
}在上面的示例中UserService 类依赖于 UserRepository 接口来保存用户信息。通过构造器注入的方式将 UserRepository 的实现类 UserRepositoryImpl 传递给 UserService从而实现了依赖注入。
使用依赖注入的好处是UserService 类不需要关心具体的 UserRepository 实现类只需要依赖于 UserRepository 接口。这样可以使得代码更加灵活可以轻松地替换不同的 UserRepository 实现类例如使用不同的数据库或者模拟数据源进行测试。
第6条避免创建不必要的对象
创建对象是有开销的包括内存分配、初始化和垃圾回收等。因此如果可以避免创建不必要的对象可以提高性能和减少内存消耗。
示例1拼接字符串
// 不推荐的写法
String result ;
for (int i 0; i 10; i) {result i;
}// 推荐的写法
StringBuilder sb new StringBuilder();
for (int i 0; i 10; i) {sb.append(i);
}
String result sb.toString();不推荐的写法使用了字符串拼接操作符 来拼接字符串每次拼接都会创建一个新的字符串对象。而推荐的写法使用了 StringBuilder 类来进行字符串拼接避免了不必要的字符串对象的创建。 示例2自动装箱和拆箱
// 不推荐的写法
Integer sum 0;
for (int i 0; i 10; i) {sum i;
}// 推荐的写法
int sum 0;
for (int i 0; i 10; i) {sum i;
}不推荐的写法使用了自动装箱和拆箱操作将 int 类型的变量转换为 Integer 类型的对象进行计算。每次装箱和拆箱都会创建新的对象导致不必要的对象创建。而推荐的写法直接使用 int 类型的变量进行计算避免了不必要的对象创建。 示例3使用静态工厂方法
包括一些我们的实体类也是
// 不推荐的写法
Date now new Date();// 推荐的写法
Date now DateUtils.getCurrentDate();不推荐的写法直接使用 new 关键字创建 Date 对象每次调用都会创建一个新的对象。而推荐的写法使用了静态工厂方法 getCurrentDate() 来获取当前日期可以复用已经创建的对象避免了不必要的对象创建。
第七条消除过期的对象引用
过期的对象引用是指已经不再需要的对象引用但仍然被保留在内存中导致内存泄漏和资源浪费。
public class Cache {private MapString, Object cache new HashMap();public void addToCache(String key, Object value) {cache.put(key, value);}public Object getFromCache(String key) {return cache.get(key);}public void removeFromCache(String key) {cache.remove(key);}
}Cache 类实现了一个简单的缓存功能使用 HashMap 来存储缓存的对象。然而如果没有及时从缓存中移除不再需要的对象引用就会导致内存泄漏。
下面是一个使用示例
Cache cache new Cache();
cache.addToCache(key1, new Object());
cache.addToCache(key2, new Object());// 从缓存中获取对象
Object obj1 cache.getFromCache(key1);
Object obj2 cache.getFromCache(key2);// 从缓存中移除对象
cache.removeFromCache(key1);// obj1 引用的对象已经不再需要但仍然被保留在缓存中通过 addToCache() 方法将两个对象添加到缓存中然后通过 getFromCache() 方法从缓存中获取对象。然而当调用 removeFromCache() 方法从缓存中移除一个对象时该对象的引用仍然被 obj1 保留导致内存泄漏。
为了消除过期的对象引用可以在不再需要对象时及时从缓存中移除。修改示例代码如下
public void removeFromCache(String key) {cache.remove(key);System.gc(); // 显式调用垃圾回收器
}修改了 removeFromCache() 方法在移除对象引用后显式调用了垃圾回收器。这样可以加速垃圾回收的过程及时释放不再需要的对象。总之消除过期的对象引用是为了避免内存泄漏和资源浪费。
第8条避免使用终结方法和清除方法
避免使用终结方法和清除方法因为它们存在一些问题和风险。以下是一些例子来说明为什么要避免使用终结方法和清除方法 不可靠的执行时机终结方法的执行时机是由Java虚拟机JVM决定的而不是由程序员控制。这意味着无法确定终结方法何时会被调用甚至可能永远不会被调用。这会导致资源无法及时释放可能会影响程序的性能和可靠性。 性能影响终结方法的执行是由JVM的垃圾回收器负责的而垃圾回收器的执行是一个相对昂贵的操作。如果一个类中定义了终结方法那么每次垃圾回收时都需要执行终结方法这会导致额外的性能开销。 安全问题终结方法的执行时机是不确定的这可能导致一些安全问题。例如如果一个对象中的终结方法打开了一个文件或网络连接但终结方法没有被及时调用那么这些资源可能会一直保持打开状态从而导致资源泄漏或安全漏洞。 替代方案Java提供了其他更可靠和更灵活的资源管理机制如try-with-resources语句和使用finally块来确保资源的释放。这些机制可以在资源使用完毕后立即释放资源而不依赖于终结方法的执行。
第9条try-with-resources优先于try-finally
建议在处理需要关闭的资源时优先使用try-with-resources语句而不是传统的try-finally块。这是因为try-with-resources语句提供了更简洁、更安全、更易读的资源管理方式。
下面是一个使用try-with-resources语句的示例代码
public void processFile(String filePath) {try (FileReader reader new FileReader(filePath);BufferedReader bufferedReader new BufferedReader(reader)) {String line;while ((line bufferedReader.readLine()) ! null) {// 处理文件内容}} catch (IOException e) {// 处理异常}
}简洁性使用try-with-resources语句可以将资源的创建和关闭操作放在同一个代码块中使代码更加简洁和易于理解。传统的try-finally块需要在try块中创建资源在finally块中关闭资源导致代码分散在不同的块中可读性较差。 安全性try-with-resources语句可以确保资源在使用完毕后被正确关闭即使在处理过程中发生异常。它会自动调用资源的close()方法来释放资源无需手动编写关闭代码。而传统的try-finally块需要手动编写关闭代码容易出现遗漏或错误的关闭操作。 支持多个资源try-with-resources语句可以同时管理多个资源只需在try语句的括号中以分号分隔多个资源的创建语句。这样可以更方便地管理多个相关资源的关闭操作避免了嵌套的try-finally块。
反例
public 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();}
}第三章 对于所有对象都通用的方法
第10条覆盖equals时请遵守通用约定
在覆盖equals方法时应该遵守的通用约定。equals方法用于比较两个对象是否相等而通用约定规定了equals方法应该满足的特定条件。建议不要覆盖equals方法自己写equals相关判断除非迫不得已。
通用约定要求equals方法具有以下特性 自反性Reflexive对于任何非null的引用值xx.equals(x)应该返回true。 对称性Symmetric对于任何非null的引用值x和y如果x.equals(y)返回true那么y.equals(x)也应该返回true。 传递性Transitive对于任何非null的引用值x、y和z如果x.equals(y)返回truey.equals(z)返回true那么x.equals(z)也应该返回true。 一致性Consistent对于任何非null的引用值x和y如果对象中的信息没有被修改那么多次调用x.equals(y)应该始终返回相同的结果。 非空性Non-nullity对于任何非null的引用值xx.equals(null)应该返回false。
下面是一个符合通用约定的equals方法的示例代码
public class Person {private String name;private int age;// 构造方法、getter和setter等省略Overridepublic boolean equals(Object obj) {if (this obj) {return true;}if (obj null || getClass() ! obj.getClass()) {return false;}Person person (Person) obj;return age person.age Objects.equals(name, person.name);}Overridepublic int hashCode() {return Objects.hash(name, age);}
}在上面的代码中我们重写了equals方法来比较Person对象的相等性。首先我们检查两个对象是否引用同一个对象如果是则返回true。然后我们检查传入的对象是否为null或者是否属于不同的类如果是则返回false。最后我们比较两个对象的属性值是否相等使用Objects.equals方法来比较字符串属性name使用运算符来比较基本类型属性age。同时我们还重写了hashCode方法来保证相等的对象具有相同的哈希码。
反例
public class Person {private String name;private int age;// 构造方法、getter和setter等省略Overridepublic boolean equals(Object obj) {if (this obj) {return true;}if (obj null) {return false;}if (getClass() ! obj.getClass()) {return false;}Person person (Person) obj;return age person.age Objects.equals(name, person.name);}Overridepublic int hashCode() {return Objects.hash(name, age);}
}在上面的反例中我们在equals方法中没有正确处理传入对象为null的情况而是直接返回false。这违反了通用约定中的非空性要求。如果传入对象为null应该返回false而不是抛出NullPointerException。
第11条覆盖 equals 时总要覆盖 hashCode
建议在覆盖equals方法时总是要同时覆盖hashCode方法。这是因为在使用哈希表如HashMap、HashSet等存储对象时hashCode方法的正确实现是非常重要的。 equals和hashCode的关系根据Java规范如果两个对象通过equals方法比较是相等的那么它们的hashCode方法应该返回相同的值。换句话说如果两个对象相等它们的哈希码应该相等。 hashCode的作用哈希码是用来确定对象在哈希表中的存储位置的。当我们向哈希表中插入一个对象时首先会计算该对象的哈希码然后根据哈希码找到对应的存储位置。如果两个对象的哈希码不同那么它们会被存储在不同的位置即使它们通过equals方法比较是相等的。 覆盖hashCode方法的规则为了保证对象在哈希表中的存储和查找的正确性我们需要确保以下规则 如果两个对象通过equals方法比较是相等的那么它们的hashCode方法应该返回相同的值。 如果两个对象通过equals方法比较是不相等的那么它们的hashCode方法返回的值可以相同也可以不同。
示例代码与上方一致。 覆盖了equals方法来比较Person对象的name和age属性是否相等。同时我们也覆盖了hashCode方法使用Objects.hash方法来计算哈希码。
反例如果我们只覆盖了equals方法而没有覆盖hashCode方法那么在使用哈希表存储对象时会出现问题。例如
Person person1 new Person(Alice, 25);
Person person2 new Person(Alice, 25);SetPerson set new HashSet();
set.add(person1);
set.add(person2);System.out.println(set.size()); // 输出2因为person1和person2被认为是不同的对象尽管person1和person2通过equals方法比较是相等的但由于没有正确实现hashCode方法它们被认为是不同的对象导致它们都被添加到了HashSet中。
第12条始终要覆盖 toString
建议始终要覆盖toString方法。toString方法用于返回对象的字符串表示它对于调试和日志记录非常有用。 toString方法的作用toString方法用于返回对象的字符串表示。它通常用于调试和日志记录可以方便地查看对象的内容。 覆盖toString方法的规则为了提供有用的字符串表示我们需要确保以下规则 返回的字符串应该包含对象的重要信息如属性值等。 返回的字符串应该是简洁、易读的方便人们阅读和理解。
示例代码
public class Person {private String name;private int age;// 构造方法、getter和setter省略Overridepublic String toString() {return Person{ name name \ , age age };}
}覆盖toString方法可以提供更有用的字符串表示方便调试和日志记录。例如
Person person new Person(Alice, 25);
System.out.println(person); // 输出Person{nameAlice, age25}反例如果我们没有覆盖toString方法那么默认的toString方法会返回对象的类名和哈希码。例如
Person person new Person(Alice, 25);
System.out.println(person); // 输出Person1f32e575第13条谨慎地覆盖clone
谨慎地覆盖clone方法。clone方法用于创建对象的副本但它存在一些问题和潜在的风险。 clone方法的作用clone方法用于创建对象的副本。它可以用于创建一个与原始对象相同的新对象但是它并不是一个安全和通用的方法。 clone方法的问题和风险 clone方法是Object类中的一个受保护的方法它需要在子类中进行覆盖才能使用。但是clone方法的设计存在一些问题它违反了面向对象的封装原则需要直接访问对象的内部状态。 clone方法返回的是一个浅拷贝即只复制了对象的引用而不是创建了一个全新的对象。这意味着如果原始对象中包含了可变的引用类型属性那么克隆对象和原始对象之间的修改会相互影响。 clone方法的使用需要非常小心因为它容易引发错误和混乱。如果不正确地使用clone方法可能会导致对象状态的不一致和意外的行为。
示例
public class Person implements Cloneable {private String name;private int age;// 构造方法、getter和setter省略Overridepublic Person clone() {try {return (Person) super.clone();} catch (CloneNotSupportedException e) {throw new AssertionError();}}
}反例如果我们不小心地使用clone方法可能会导致意外的行为。例如如果对象中包含了可变的引用类型属性那么克隆对象和原始对象之间的修改会相互影响。示例如下
public class Person implements Cloneable {private String name;private ListString hobbies;// 构造方法、getter和setter省略Overridepublic Person clone() {try {return (Person) super.clone();} catch (CloneNotSupportedException e) {throw new AssertionError();}}
}Person person1 new Person();
person1.setName(Alice);
person1.setHobbies(new ArrayList(Arrays.asList(reading, swimming)));Person person2 person1.clone();
person2.getHobbies().add(hiking);System.out.println(person1.getHobbies()); // 输出[reading, swimming, hiking]
System.out.println(person2.getHobbies()); // 输出[reading, swimming, hiking]在上面的例子中我们克隆了person1对象得到了person2对象。然后我们向person2对象的hobbies列表中添加了一个新的爱好。结果person1对象的hobbies列表也被修改了这是因为克隆对象和原始对象共享了同一个引用类型属性。
因此为了避免clone方法的问题和风险我们应该谨慎地使用它或者考虑使用其他方式来创建对象的副本如拷贝构造函数或工厂方法。
1、拷贝构造函数:
拷贝构造函数是一个特殊的构造函数它接受一个相同类型的对象作为参数并使用该对象的属性值来初始化新对象。通过拷贝构造函数我们可以创建一个与原始对象相同的新对象。
public class Person {private String name;private int age;// 拷贝构造函数public Person(Person other) {this.name other.name;this.age other.age;}// 构造方法、getter和setter省略
}Person person1 new Person(Alice, 25);
Person person2 new Person(person1); // 使用拷贝构造函数创建副本System.out.println(person1.getName()); // 输出Alice
System.out.println(person2.getName()); // 输出Alice2、工厂方法
工厂方法是一个静态方法它返回一个新的对象作为副本。通过工厂方法我们可以更加灵活地创建对象的副本可以在方法中进行必要的处理和验证。
public class Person {private String name;private int age;// 构造方法、getter和setter省略// 工厂方法public static Person createCopy(Person other) {Person copy new Person();copy.setName(other.getName());copy.setAge(other.getAge());return copy;}
}Person person1 new Person(Alice, 25);
Person person2 Person.createCopy(person1); // 使用工厂方法创建副本System.out.println(person1.getName()); // 输出Alice
System.out.println(person2.getName()); // 输出Alice第14条考虑实现Comparable接口
Comparable接口是Java中的一个泛型接口用于定义对象之间的自然排序顺序。实现Comparable接口的主要目的是为了使对象可以进行比较和排序。通过实现Comparable接口我们可以定义对象之间的比较规则并使用排序算法对对象进行排序。
示例
public class Person implements ComparablePerson {private String name;private int age;public Person(String name, int age) {this.name name;this.age age;}// 实现compareTo方法定义对象之间的比较规则Overridepublic int compareTo(Person other) {// 按照年龄进行比较return Integer.compare(this.age, other.age);}// 其他代码和方法省略...
}通过实现Comparable接口我们可以使用排序算法对Person对象进行排序例如使用Collections.sort方法
ListPerson personList new ArrayList();
personList.add(new Person(Alice, 25));
personList.add(new Person(Bob, 30));
personList.add(new Person(Charlie, 20));Collections.sort(personList);for (Person person : personList) {System.out.println(person.getName() - person.getAge());
}输出结果将按照年龄从小到大的顺序进行排序。
建议每当实现一个对排序敏感的类时都应该让这个类实现Comparable接口以便其实例可以轻松地被分类、搜索以及用在基于比较的集合种。每当在compareTo方法的实现中比较域值时都要避免使用 和 操作符而应该在装修基本类型中类中使用静态的compare方法或者在Comparator接口中使用比较器构造方法。
第四章 类和接口
类和接口是Java编程语言的核心它们也是Java语言的基本抽象单元。Java语言提供了许多强大的基本元素供程序员用来设计类和接口。本章阐述的一些指导原则可以帮助你更好地利用这些元素设计出更加有用、健壮和灵活的类和接口。
第15条使类和成员的可访问性最小化
这个原则是指在设计类和成员时应该尽量将其访问权限限制在最小范围内以提高封装性、安全性和可维护性。
具体来说可以采取以下几个方面的措施来最小化类和成员的可访问性 将类声明为final或abstract如果一个类不打算被继承可以将其声明为final这样其他类就无法继承它。如果一个类只作为父类存在不打算被直接实例化可以将其声明为abstract。这样可以限制类的访问范围避免不必要的继承和实例化。 将成员声明为private将类的成员字段、方法等声明为private只允许类内部访问。这样可以隐藏实现细节防止外部直接访问和修改类的内部状态。 提供有限的访问接口通过将部分成员声明为public或protected提供有限的访问接口使外部只能通过这些接口来访问类的功能。这样可以控制对类的访问权限避免外部直接访问类的内部实现细节。 使用包级私有访问将类和成员声明为包级私有即不加访问修饰符只允许同一个包中的其他类访问。这样可以限制类的访问范围避免被其他包中的类访问和修改。
示例
public class MyClass {private int privateField;public int publicField;private void privateMethod() {// 私有方法的实现}public void publicMethod() {// 公共方法的实现}
}在上面的示例中privateField和privateMethod被声明为private只能在MyClass类内部访问。publicField和publicMethod被声明为public可以被其他类直接访问。
通过将成员的访问权限限制在最小范围内我们可以提高类的封装性隐藏实现细节减少对外部的依赖提高代码的安全性和可维护性。同时这也是一种良好的设计原则符合面向对象编程的封装思想。
第16条要在公有类中使用访问方法而非公有域
这个原则是指在设计公有类时应该尽量避免直接暴露类的内部数据域而是通过访问方法来访问和修改数据。
使用访问方法也称为getter和setter方法的好处有以下几点 封装性通过使用访问方法可以将类的内部数据域隐藏起来只允许通过方法来访问和修改数据。这样可以提高类的封装性隐藏实现细节减少对外部的依赖。 可控性通过访问方法可以对数据的访问和修改进行控制。可以在方法中添加逻辑判断、数据验证等确保数据的有效性和一致性。 可扩展性通过使用访问方法可以在不改变类的接口的前提下对类的内部实现进行修改。如果直接暴露数据域一旦需要修改数据的表示方式或实现逻辑就会破坏类的兼容性。
示例
public class Person {private String name;private int age;public String getName() {return name;}public void setName(String name) {this.name name;}public int getAge() {return age;}public void setAge(int age) {if (age 0) {throw new IllegalArgumentException(Age cannot be negative);}this.age age;}
}在上面的示例中Person类有两个私有的数据域name和age。通过提供getName和setName方法来访问和修改name提供getAge和setAge方法来访问和修改age。通过使用访问方法我们可以对数据的访问和修改进行控制确保数据的有效性和一致性。
反例
public class Person {public String name;public int age;
}在上面的反例中Person类的数据域name和age被声明为公有的外部可以直接访问和修改这些数据。这样做的问题是外部可以随意修改数据没有任何限制和验证。这可能导致数据的不一致和不安全。
第17条使可变性最小化
这个原则是指在设计类时应该尽量将类的可变性限制在最小范围内以提高类的安全性和可维护性。
使用不可变类的好处有以下几点 线程安全不可变类是线程安全的因为它们的状态不会发生变化所以多个线程可以同时访问不可变对象而不需要额外的同步措施。 安全性不可变类不可被修改这意味着它们的状态是固定的不会被意外或恶意修改。这可以防止一些潜在的安全问题。 可重用性不可变类可以被自由地共享和重用因为它们的状态不会发生变化。这可以提高代码的性能和效率。
示例
public final class Point {private final int x;private final int y;public Point(int x, int y) {this.x x;this.y y;}public int getX() {return x;}public int getY() {return y;}
}在上面的示例中Point类是一个不可变类它有两个私有的final字段x和y它们在对象创建后就不可被修改。通过提供getX和getY方法来访问这些字段的值。由于Point类是不可变的所以它是线程安全的可以被自由地共享和重用。
反例
public class MutablePoint {private int x;private int y;public MutablePoint(int x, int y) {this.x x;this.y y;}public int getX() {return x;}public void setX(int x) {this.x x;}public int getY() {return y;}public void setY(int y) {this.y y;}
}在上面的反例中MutablePoint类是一个可变类它的字段x和y可以被外部修改。这样做的问题是外部可以随意修改对象的状态可能导致对象的不一致和不安全。
对于某些类而言其不可变性是不切实际的如果类不能被做成不可变的仍然应该尽可能地限制它的可变性。除非有令人信服的理由要使域变成是非final的否则要使每个域都是private final的。
第18条复合优先于继承
这个原则强调了在设计类和对象之间的关系时应该优先选择使用复合Composition而不是继承Inheritance。
复合是指通过在一个类中包含其他类的实例来构建更复杂的对象。这种方式可以实现代码的重用和灵活性同时避免了继承可能带来的一些问题。
以下是一些使用复合而不是继承的优点 灵活性复合允许在运行时动态地改变对象的行为而不需要在编译时确定。这使得代码更加灵活可以根据需求进行扩展和修改。 代码重用通过将功能封装在独立的类中可以在多个类之间共享和重用代码。这样可以减少代码的重复提高代码的可维护性和可读性。 松耦合使用复合可以实现松耦合的设计即对象之间的依赖关系更加灵活和可配置。这样可以减少类之间的耦合度提高代码的可测试性和可扩展性。
示例
public class Car {private Engine engine;private Wheels wheels;public Car(Engine engine, Wheels wheels) {this.engine engine;this.wheels wheels;}public void start() {engine.start();}public void drive() {wheels.rotate();}
}在上面的示例中Car类通过包含一个Engine对象和一个Wheels对象来实现其功能。这种复合的方式允许我们在运行时选择不同的引擎和轮子从而改变汽车的行为。
简而言之继承的功能非常强大但是也存在诸多问题因为它违背了封装原则。只有当子类和超类之间确实存在子类型关系时使用继承才是恰当的。即便如此如果子类和超类处在不同的包中并且超类并不是为了继承而设计的那么继承将会导致脆弱性。为了避免这种脆弱性可以用复合和转发机制来代替继承尤其是当存在适当的接口可以实现包装类的时候。包装类不仅比子类更加健壮而且功能更加强大。
第19条要么设计继承并提供文档说明要么禁止继承
这个原则强调了在设计类时应该明确地决定是否允许其他类继承该类并相应地进行设计和文档说明。
以下是一些关于这个原则的要点 明确设计如果一个类被设计为可继承的那么它应该提供适当的扩展点和可覆盖的方法。这样可以确保子类可以正确地扩展和定制父类的行为。 提供文档说明如果一个类被设计为可继承的那么应该在文档中明确说明该类的设计意图、可继承的方法和行为的约束。这样可以帮助其他开发人员正确地使用和扩展该类。 禁止继承如果一个类不适合被继承那么应该使用final关键字将其声明为最终类或者将所有的构造函数声明为私有的以防止其他类继承该类。这样可以避免其他开发人员错误地继承和修改该类。
示例
public class Shape {protected int x;protected int y;public Shape(int x, int y) {this.x x;this.y y;}public void draw() {// 绘制形状的逻辑}/*** 计算形状的面积* return 面积*/public double calculateArea() {// 计算面积的逻辑}
}在上面的示例中Shape类被设计为可继承的并提供了文档说明。子类可以继承Shape类并根据需要扩展和定制其行为。
总而言之要么设计继承并提供文档说明要么禁止继承的原则强调了在设计类时应该明确地决定是否允许继承并相应地进行设计和文档说明。这样可以避免在不适合继承的类上使用继承从而减少潜在的问题和困惑。
第20条接口优于抽象类
接口具有一些优势可以提供更大的灵活性和可复用性。下面是对这条建议的详细说明以及一些例子和反例
1、接口提供更大的灵活性
接口可以被多个类实现而类只能继承一个抽象类。这意味着使用接口可以更灵活地组合和扩展功能。接口支持多重继承一个类可以实现多个接口但是只能继承一个抽象类。这使得接口可以更好地支持多个功能的组合。
2、接口提供更好的可复用性
接口可以被多个类实现从而提供更好的可复用性。如果一个类需要实现多个功能可以通过实现多个接口来实现而不是继承多个抽象类。
例子
interface Flyable {void fly();
}interface Swimmable {void swim();
}class Bird implements Flyable {public void fly() {System.out.println(Bird is flying);}
}class Fish implements Swimmable {public void swim() {System.out.println(Fish is swimming);}
}class Duck implements Flyable, Swimmable {public void fly() {System.out.println(Duck is flying);}public void swim() {System.out.println(Duck is swimming);}
}反例如果使用抽象类来实现上述功能就无法同时让Duck类既能飞又能游泳。
总结接口提供了更大的灵活性、支持多重继承和更好的可复用性。但并不是所有情况下都适合使用接口而不是抽象类。有时候抽象类更适合例如需要提供默认实现或强制子类实现某些方法的情况。在设计时需要根据具体的需求和情况来选择使用接口还是抽象类。
第21条为后代设计接口
这意味着在设计接口时要预测未来可能的变化和扩展并为后代提供足够的灵活性和可扩展性。
1、考虑后代的需求
在设计接口时要考虑后代可能需要添加新的方法或功能。接口应该提供足够的灵活性以便后代可以轻松地扩展接口而不破坏现有的实现类。接口的设计应该是稳定的避免频繁的修改和变化。这样可以确保后代的实现类不会受到不必要的影响。
2、提供默认实现
在设计接口时可以提供一些默认的方法实现以便后代可以选择性地覆盖这些方法。这样可以在不破坏现有实现类的情况下为后代提供一些通用的功能。
示例
interface Drawable {void draw();default void fill() {System.out.println(Filling the shape);}
}class Rectangle implements Drawable {public void draw() {System.out.println(Drawing a rectangle);}
}class Circle implements Drawable {public void draw() {System.out.println(Drawing a circle);}public void fill() {System.out.println(Filling the circle);}
}总结在设计接口时要考虑后代的需求提供足够的灵活性和可扩展性。可以通过提供默认实现和稳定的接口设计来实现这一点。但是也要注意不要过度设计接口避免频繁的修改和变化。在设计时需要根据具体的需求和情况来平衡灵活性和稳定性。
第22条接口只用于定义类型
当类实现接口时接口就充当可以引用这个类的实例的类型。因此类实现了接口就表明客户端可以对这个类的实例实施某些动作。为了任何其他目的而定义接口是不恰当的。
常量接口模式是对接口的不良使用。常量接口模式是指在接口中定义一些常量并且实现类通过实现该接口来获取这些常量。这种模式的问题在于它违反了接口只用于定义类型的原则。
常量接口示例
public final class Constants {public static final int MAX_LENGTH 100;public static final int MIN_LENGTH 10;public static final String DEFAULT_NAME John Doe;// 其他常量定义...
}常量接口模式的问题有以下几点
接口应该用于定义类型而不是用于定义常量。常量应该与具体的类或者枚举相关联而不是与接口相关联。将常量定义在接口中会导致实现类必须实现这些常量即使它们与实现类的逻辑无关。这增加了实现类的负担并且可能导致不必要的代码冗余。如果接口中的常量发生变化所有实现该接口的类都必须重新编译和部署即使它们与这些常量无关。
使用常量类相比使用常量接口有以下几个好处 避免命名冲突常量类中的常量是通过类名来访问的因此可以避免不同模块之间的命名冲突。如果不同的模块定义了相同的常量编译器会报错提醒开发人员解决冲突。而常量接口中的常量是通过接口名来访问的如果不同的模块定义了相同的常量编译器不会报错可能会导致混淆和错误的使用。 提供类型安全常量类中的常量是静态的可以直接通过类名访问而常量接口中的常量是隐式的公共静态常量需要通过实现接口的类来访问。使用常量类可以提供类型安全编译器可以在编译时检查常量的类型和使用方式减少类型错误的可能性。 提高代码的可读性和可维护性常量类可以将相关的常量集中在一个类中使得代码更加清晰和易于理解。开发人员可以直接通过常量类来查找和使用常量而不需要在代码中硬编码常量值。而常量接口中的常量可能分散在不同的实现类中不易于查找和维护。 隐藏实现细节常量类可以隐藏常量的实现细节只暴露常量的访问接口。这样可以在不影响使用常量的地方修改常量的实现而不需要修改调用方的代码。而常量接口将常量的实现细节暴露给了实现类修改常量的实现可能需要修改调用方的代码。
因此常量接口模式不推荐使用。相反应该将常量定义在具体的类或者枚举中以便与其相关的逻辑和语义。如果需要在多个类中共享常量可以将它们定义在一个工具类中或者使用静态导入来直接使用常量。
第23条类层次优于标签类
标签类是指一个类中包含一个标签字段tag field用于标识对象所属的类型然后根据标签字段的值来执行相应的逻辑。而类层次结构是指通过继承和多态来表示不同类型的对象每个子类都代表一个具体的类型而不需要使用标签字段。
标签类代码示例 根据标签type字段判断不同类型不同逻辑方法。
public class Shape {private String type;private double radius;private double width;private double height;public Shape(String type) {this.type type;}public void setRadius(double radius) {this.radius radius;}public void setWidth(double width) {this.width width;}public void setHeight(double height) {this.height height;}public double calculateArea() {if (type.equals(circle)) {return Math.PI * radius * radius;} else if (type.equals(rectangle)) {return width * height;}return 0;}
}标签类过于冗长容易出错并且效率低下。
类层次代码示例
public abstract class Shape {public abstract double calculateArea();
}public class Circle extends Shape {private double radius;public Circle(double radius) {this.radius radius;}Overridepublic double calculateArea() {return Math.PI * radius * radius;}
}public class Rectangle extends Shape {private double width;private double height;public Rectangle(double width, double height) {this.width width;this.height height;}Overridepublic double calculateArea() {return width * height;}
}使用类层次结构的代码更加清晰和易于理解。每个子类都有自己的 calculateArea() 方法来计算面积不需要使用标签字段来判断对象的类型。当需要添加新的图形类型时只需要添加一个新的子类即可不需要修改现有的代码。
使用类层次结构而不是标签类有以下几个优点 可读性更好使用类层次结构可以更清晰地表达对象的类型和行为。每个子类都有自己的特定行为和属性代码更易于理解和维护。 可扩展性更好当需要添加新的类型时只需要添加一个新的子类即可而不需要修改现有的代码。这符合开闭原则Open-Closed Principle即对扩展开放对修改关闭。 类型安全性更高使用类层次结构可以在编译时进行类型检查减少类型错误的可能性。而标签类需要在运行时通过标签字段的值来确定对象的类型容易出现类型错误。
第24条静态成员类由于非静态成员类
静态成员类是指被声明为静态的内部类它与外部类之间没有直接的关联可以独立存在。而非静态成员类是指没有被声明为静态的内部类它与外部类之间有直接的关联可以访问外部类的成员。
下面是一个示例演示了静态成员类和非静态成员类的区别
public class OuterClass {private static int outerStaticField;private int outerNonStaticField;// 静态成员类public static class StaticMemberClass {private int staticMemberField;public void staticMemberMethod() {// 可以访问外部类的静态成员outerStaticField 10;// 不能访问外部类的非静态成员// outerNonStaticField 20; // 编译错误}}// 非静态成员类public class NonStaticMemberClass {private int nonStaticMemberField;public void nonStaticMemberMethod() {// 可以访问外部类的静态成员和非静态成员outerStaticField 10;outerNonStaticField 20;}}
}使用静态成员类而不是非静态成员类有以下几个优点 更好的封装性静态成员类可以被外部类以外的代码访问而非静态成员类只能被外部类的实例访问。这样可以更好地控制类的可见性提高封装性。 减少内存占用非静态成员类会隐式地持有外部类的引用导致内存占用增加。而静态成员类不会持有外部类的引用可以减少内存占用。 更清晰的代码结构将静态成员类作为外部类的静态成员可以更清晰地表示它们之间的关系提高代码的可读性。 独立性和可复用性静态成员类可以独立存在不依赖于外部类的实例。这使得静态成员类可以更容易地被其他类使用和复用。静态成员类可以在不同的上下文中使用而不需要依赖于外部类的实例。
第25条限制源文件为单个顶级类
每个源文件中只能包含一个顶级类并且该类的名称必须与文件名相同。
假设我们有两个顶级类Person和Address。按照建议我们应该将它们分别放在两个不同的源文件中person类和address类中不要两个类放在同一个源文件中
public class Person {// 类的定义和实现
}public class Address {// 类的定义和实现
}这个建议的目的是为了提高代码的可读性和可维护性。将每个顶级类放在单独的源文件中可以使代码更加清晰和易于理解。以下是一些详细说明和示例 可读性将每个顶级类放在单独的源文件中可以使代码结构更加清晰。开发人员可以更容易地找到特定类的定义和实现。这样可以提高代码的可读性减少阅读和理解代码的难度。 维护性将每个顶级类放在单独的源文件中可以使代码的修改和维护更加方便。当需要修改一个类时只需要打开对应的源文件而不需要在一个文件中查找和编辑多个类的定义。这样可以减少出错的可能性提高代码的可维护性。
第五章 泛型
从Java 5开始泛型已经成了Java编程语言的一部分。在没有泛型之前从集合中读取到的每一个对象都必须进行转换。如果有人不小心插入了类型错误的对象在运行时的转换处理就会出错。有了泛型之后你可以告诉编译器每个集合中接受哪些对象类型。编译器自动为你的插入进行转换并在编译时告知是否插入了类型错误的对象。这样可以使程序更加安全、也更加清楚但是要享有这些有试不限于集合有一定的难度。本章就是教你如何最大限度地享有这些优势又能使整个过程尽可能简单化。
第26条请不要使用原生态类型
原生态类型是指没有指定类型参数的泛型类或泛型接口的使用方式。
原生态类型示例
List list new ArrayList();
list.add(Hello);
list.add(10);for (Object item : list) {String str (String) item; // 运行时会抛出ClassCastExceptionSystem.out.println(str);
}原生态类型问题
缺乏类型安全性原生态类型会导致编译器无法对类型进行检查从而可能引发运行时错误。缺乏可读性使用原生态类型会使代码的意图不明确降低代码的可读性。泛型的目的是为了在编译时提供类型安全性和更好的代码可读性。使用原生态类型会使代码中的类型信息丢失使得其他开发人员难以理解代码的意图。
为了避免使用原生态类型应该始终使用泛型并为泛型类或泛型接口提供类型参数。例如对于上述代码应该使用泛型类型List来明确列表中的元素类型
ListString list new ArrayList();
list.add(Hello);
list.add(10); // 编译时会报错for (String item : list) {System.out.println(item);
}第27条消除非受检的警告
消除非受检的警告的主要目的是确保代码的类型安全性并减少在运行时可能出现的错误。
List list new ArrayList();
list.add(Hello);String str (String) list.get(0); // 非受检的警告System.out.println(str);在这个例子中我们使用了原生态类型List并将元素强制转换为String类型。这会导致编译器发出非受检的警告因为编译器无法确定列表中的元素类型是否为String。为了消除这个警告我们可以使用泛型类型List来明确列表中的元素类型
ListString list new ArrayList();
list.add(Hello);String str list.get(0);System.out.println(str);使用注解在某些情况下可以使用注解来消除非受检的警告。例如考虑以下代码
SuppressWarnings(unchecked)
List list new ArrayList();
list.add(Hello);String str (String) list.get(0);System.out.println(str);在这个例子中我们使用了SuppressWarnings(“unchecked”)注解来告诉编译器忽略非受检的警告。虽然这种方法可以消除警告但它并不是最佳实践。应该尽量避免使用注解来消除警告而是通过改进代码来消除警告。
第28条列表优于数组
建议在大多数情况下使用列表List而不是数组Array。列表是一种动态大小的数据结构可以方便地添加、删除和访问元素而数组的大小是固定的。
列表的相比于数组的好处
灵活性列表具有更大的灵活性可以根据需要动态调整大小。您可以使用列表的方法如add、remove、set来添加、删除和修改元素而数组的大小是固定的无法直接添加或删除元素。
ListString list new ArrayList();
list.add(apple);
list.add(banana);
list.remove(0);类型安全性列表可以使用泛型来指定元素的类型从而提供类型安全性。这意味着您可以在编译时捕获类型错误而不是在运行时出现错误。数组没有类型检查因此可能会导致运行时错误。
ListString list new ArrayList();
list.add(apple);
list.add(123); // 编译错误类型不匹配String[] array new String[2];
array[0] apple;
array[1] 123; // 运行时错误类型不匹配功能丰富列表提供了许多有用的方法和功能如排序、查找、迭代等。这些方法可以方便地操作和处理列表中的元素。而数组的功能相对较少需要手动编写代码来实现这些功能。与泛型的兼容性列表与泛型类型更好地兼容。您可以使用通配符wildcard来处理不同类型的列表而数组只能存储同一类型的元素。
List? list new ArrayList();
list.add(apple);
list.add(123);
list.add(true);ListString stringList new ArrayList();
stringList.add(apple);
stringList.add(banana);ListInteger integerList new ArrayList();
integerList.add(1);
integerList.add(2);List[] array new List[2];
array[0] stringList;
array[1] integerList;第29条优先考虑泛型
强调在编写代码时应优先考虑使用泛型来增加代码的类型安全性和重用性。
使用泛型的好处
类型安全泛型可以在编译时捕获类型错误避免在运行时出现类型转换异常。代码重用泛型可以使代码更通用可以在不同类型之间进行重用减少代码的重复编写。可读性和可维护性泛型可以提供更清晰的代码使代码更易读和维护。
泛型类
public class BoxT {private T item;public void setItem(T item) {this.item item;}public T getItem() {return item;}
}在这个示例中Box类是一个泛型类使用类型参数T来表示存储的物品的类型。通过使用泛型我们可以在编译时检查存储的物品的类型并避免类型错误。
泛型方法
public T T getFirstElement(ListT list) {if (list.isEmpty()) {throw new NoSuchElementException();}return list.get(0);
}在这个示例中getFirstElement方法是一个泛型方法使用类型参数T来表示列表中元素的类型。通过使用泛型方法我们可以在编译时检查传入的列表的元素类型并返回第一个元素。
通配符类型
public void printList(List? list) {for (Object item : list) {System.out.println(item);}
}在这个示例中printList方法接受一个通配符类型的列表作为参数。通过使用通配符类型我们可以接受任意类型的列表作为参数并在方法内部进行遍历和打印。
第30条优先考虑泛型方法
它强调在编写代码时应优先考虑使用泛型方法来增加代码的灵活性和可读性。
使用泛型方法的好处参考上面第29条。
1、泛型方法
public T T getFirstElement(ListT list) {if (list.isEmpty()) {throw new NoSuchElementException();}return list.get(0);
}在这个示例中getFirstElement方法是一个泛型方法使用类型参数T来表示列表中元素的类型。通过使用泛型方法我们可以在编译时检查传入的列表的元素类型并返回第一个元素。
2、泛型方法与多个类型参数
public K, V MapK, V createMap(K key, V value) {MapK, V map new HashMap();map.put(key, value);return map;
}createMap方法是一个泛型方法使用类型参数K和V来表示键和值的类型。通过使用泛型方法和多个类型参数我们可以创建一个具有指定键和值类型的Map对象。
第31条利用有限制通配符来提升API的灵活性
在Java中通配符wildcard用于表示未知类型。有限制通配符是指在通配符后面添加了上界或下界的通配符。上界通配符使用extends关键字表示通配符可以是指定类型或其子类型。下界通配符使用super关键字表示通配符可以是指定类型或其父类型。
下面是一些使用有限制通配符的示例
1、有限制通配符作为方法参数
public static void printList(List? extends Number list) {for (Number number : list) {System.out.println(number);}
}在这个示例中printList方法接受一个List类型的参数该参数的元素类型必须是Number或其子类。通过使用有限制通配符? extends Number我们可以在方法内部安全地使用Number类型的方法而不会对集合进行非法操作。
2、有限制通配符作为方法返回类型
public static T extends ComparableT T max(ListT list) {if (list.isEmpty()) {throw new IllegalArgumentException(List is empty);}T max list.get(0);for (int i 1; i list.size(); i) {T current list.get(i);if (current.compareTo(max) 0) {max current;}}return max;
}在这个示例中max方法接受一个List类型的参数并返回该列表中的最大元素。通过使用有限制通配符T extends Comparable我们可以确保列表中的元素类型实现了Comparable接口从而可以使用compareTo方法进行比较。
通过使用有限制通配符我们可以提高API的灵活性使其适用于更广泛的类型参数。它可以帮助我们编写更通用的方法并提高代码的可读性和安全性。因此在编写API时应优先考虑使用有限制通配符来获得这些好处。
第32条谨慎并用泛型和可变参数
使用泛型时要注意类型安全性。泛型可以提供编译时的类型检查但在运行时会擦除类型信息。因此在使用泛型时要确保类型参数的正确性并避免出现类型转换错误。
例如考虑以下示例
public class GenericExampleT {private T value;public void setValue(T value) {this.value value;}public T getValue() {return value;}
}GenericExampleString example new GenericExample();
example.setValue(Hello);
String value example.getValue(); // 正确返回类型为Stringexample.setValue(123); // 错误编译时会报错因为类型参数为String在使用可变参数时要注意参数的安全性。可变参数允许传入任意数量的参数但在处理可变参数时要小心。 例如考虑以下示例
public void printValues(String... values) {for (String value : values) {System.out.println(value);}
}printValues(Hello, World); // 正确输出Hello和WorldprintValues(Hello, World, 123); // 错误编译时不会报错但在运行时会抛出ClassCastException异常第33条优先考虑类型安全的异构容器
建议优先考虑使用类型安全的异构容器以提高代码的可读性和类型安全性。
异构容器是指可以存储不同类型的对象的容器。传统的容器类如List、Map等都是同构容器即只能存储相同类型的对象。而异构容器可以存储不同类型的对象但要求在编译时就能够确定每个对象的类型。
使用类型安全的异构容器可以避免在运行时进行类型转换提供更好的类型安全性和代码可读性。下面是一个示例
public class HeterogeneousContainer {private MapClass?, Object container new HashMap();public T void put(ClassT type, T instance) {container.put(type, instance);}public T T get(ClassT type) {return type.cast(container.get(type));}
}在上述示例中HeterogeneousContainer类使用了一个Map来存储不同类型的对象。通过put方法可以将对象按照其类型存入容器中而get方法可以根据类型获取对应的对象。
使用异构容器的好处是可以在编译时就能够确定每个对象的类型避免了在运行时进行类型转换的风险。同时通过使用泛型和类型参数可以保证在获取对象时返回正确的类型提高了代码的可读性和可维护性。
使用异构容器的一个典型应用场景是在框架中存储和管理不同类型的插件或扩展点。通过使用异构容器可以方便地将不同类型的插件注册到框架中并在需要时获取对应类型的插件实例。
总之优先考虑使用类型安全的异构容器可以提高代码的可读性和类型安全性避免在运行时进行类型转换的风险。
第六章 枚举和注解
Java支持两种特殊用途的引用类型一个是类称作枚举类型一种是接口称作注解类型。本章将讨论这两个新类型的最佳使用实践。
第34条用enum代替int常量
建议使用枚举(enum)类型来代替int常量以提高代码的可读性、类型安全性和可维护性。
使用枚举类型可以将一组相关的常量值组织在一起并为每个常量值提供更多的信息和行为。相比于使用int常量使用枚举类型可以提供更好的类型安全性避免了使用错误的常量值。
下面是一个示例
public enum DayOfWeek {MONDAY(星期一),TUESDAY(星期二),WEDNESDAY(星期三),THURSDAY(星期四),FRIDAY(星期五),SATURDAY(星期六),SUNDAY(星期日);private String chineseName;DayOfWeek(String chineseName) {this.chineseName chineseName;}public String getChineseName() {return chineseName;}
}在上述示例中DayOfWeek是一个枚举类型表示一周的每一天。每个枚举常量都有一个对应的中文名称并通过构造函数进行初始化。枚举类型还可以定义其他方法以提供更多的行为。
使用枚举类型的好处
可以提供更好的可读性和类型安全性。在使用枚举常量时可以直接使用枚举类型的名称而不需要记住对应的int常量值。编译器会在编译时进行类型检查避免了使用错误的常量值的风险。枚举类型还可以方便地进行扩展和添加新的常量值
总之使用枚举类型来代替int常量可以提高代码的可读性、类型安全性和可维护性。枚举类型可以将一组相关的常量值组织在一起并为每个常量值提供更多的信息和行为。
第35条用实例域代替序数
建议使用实例域代替枚举的序数ordinal以提高代码的可读性和可维护性。
枚举的序数是指每个枚举常量在枚举中的位置从0开始计数。默认情况下枚举类型提供了一个ordinal()方法来获取枚举常量的序数。然而使用枚举的序数来表示枚举常量的属性或行为是不推荐的因为它具有以下问题
序数是基于枚举常量的位置如果枚举常量的顺序发生变化序数也会发生变化导致代码的可读性和可维护性变差。序数是从0开始计数的不具有可读性不容易理解。
为了解决这些问题作者建议使用实例域来代替序数。通过在枚举类型中定义实例域并在构造函数中进行初始化可以为每个枚举常量提供更多的属性和行为。
下面是一个示例
public enum DayOfWeek {MONDAY(星期一, 1),TUESDAY(星期二, 2),WEDNESDAY(星期三, 3),THURSDAY(星期四, 4),FRIDAY(星期五, 5),SATURDAY(星期六, 6),SUNDAY(星期日, 7);private String chineseName;private int value;DayOfWeek(String chineseName, int value) {this.chineseName chineseName;this.value value;}public String getChineseName() {return chineseName;}public int getValue() {return value;}
}在上述示例中DayOfWeek是一个枚举类型表示一周的每一天。每个枚举常量都有一个对应的中文名称和一个值。通过定义实例域chineseName和value可以为每个枚举常量提供更多的属性。通过定义相应的getter方法可以获取枚举常量的属性值。
使用实例域代替序数的好处是可以提高代码的可读性和可维护性。通过使用具有可读性的实例域可以更清晰地表示枚举常量的属性。同时实例域不受枚举常量顺序的影响即使枚举常量的顺序发生变化代码也不会受到影响。
总之使用实例域代替序数可以提高代码的可读性和可维护性。通过在枚举类型中定义实例域并在构造函数中进行初始化可以为每个枚举常量提供更多的属性和行为。这样可以避免使用不具有可读性的序数并提供更清晰的代码结构。
第36条用EnumSet代替位域
建议使用EnumSet代替位域bit fields来表示包含枚举值的集合。使用EnumSet可以提供更好的类型安全性、可读性和性能。
位域是使用整数类型来表示一组枚举值的集合。例如假设我们有一个表示权限的枚举类型
public enum Permission {READ,WRITE,EXECUTE
}使用位域来表示权限集合可以将每个权限映射到一个位上然后使用位运算来进行集合操作。例如使用一个int类型的变量来表示权限集合
public static final int READ_PERMISSION 1 0; // 1
public static final int WRITE_PERMISSION 1 1; // 2
public static final int EXECUTE_PERMISSION 1 2; // 4int permissions READ_PERMISSION | WRITE_PERMISSION; // 3然而使用位域存在一些问题。首先位域没有类型安全性可以将任意整数值赋给位域变量即使该值不是有效的权限值。其次位域的可读性较差不容易理解和维护。最后位域的性能可能较差特别是在进行集合操作时。
相比之下EnumSet提供了更好的解决方案。EnumSet是一个专门用于存储枚举值的集合的高效实现。它具有以下优点
类型安全性EnumSet只能存储指定枚举类型的值提供了更好的类型安全性。可读性EnumSet的代码更易读可以直接使用枚举值进行操作而不需要进行位运算。性能EnumSet在内部使用位向量bit vector来表示集合因此具有很高的性能。
下面是使用EnumSet来表示权限集合的示例
import java.util.EnumSet;public class Example {public enum Permission {READ,WRITE,EXECUTE}public static void main(String[] args) {EnumSetPermission permissions EnumSet.of(Permission.READ, Permission.WRITE);System.out.println(permissions); // [READ, WRITE]}
}在上面的示例中我们使用EnumSet.of()方法创建了一个包含READ和WRITE权限的EnumSet对象。通过直接使用枚举值我们可以更清晰地表示和操作权限集合。
总之根据《Effective Java》的建议使用EnumSet代替位域可以提供更好的类型安全性、可读性和性能。它是表示包含枚举值的集合的首选方式。
第37条用EnumMap代替序数索引
建议使用EnumMap代替序数索引来实现基于枚举的数据结构。使用EnumMap可以提供更好的类型安全性、可读性和性能。
序数索引是指使用枚举值的序数ordinal作为数组或列表的索引来存储和访问数据。例如假设我们有一个表示星期几的枚举类型
public enum DayOfWeek {MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY
}使用序数索引来存储和访问数据可以创建一个数组或列表并使用枚举值的序数作为索引
String[] tasks new String[7];
tasks[DayOfWeek.MONDAY.ordinal()] Do laundry;
tasks[DayOfWeek.TUESDAY.ordinal()] Go grocery shopping;
// ...然而使用序数索引存在一些问题。首先序数索引没有类型安全性可以使用任意整数值作为索引即使该值不是有效的枚举序数。其次序数索引的可读性较差不容易理解和维护。最后序数索引的性能可能较差特别是在稀疏数组或大型数组的情况下。
相比之下EnumMap提供了更好的解决方案。EnumMap是一个专门用于存储枚举类型键值对的高效实现。它具有以下优点
类型安全性EnumMap只能存储指定枚举类型的键值对提供了更好的类型安全性。可读性EnumMap的代码更易读可以直接使用枚举值作为键进行操作而不需要使用序数。性能EnumMap在内部使用数组来表示键值对因此具有很高的性能尤其在稀疏数组或大型数组的情况下。
下面是使用EnumMap来存储星期几对应任务的示例
import java.util.EnumMap;public class Example {public enum DayOfWeek {MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY}public static void main(String[] args) {EnumMapDayOfWeek, String tasks new EnumMap(DayOfWeek.class);tasks.put(DayOfWeek.MONDAY, Do laundry);tasks.put(DayOfWeek.TUESDAY, Go grocery shopping);// ...System.out.println(tasks.get(DayOfWeek.MONDAY)); // Do laundry}
}在上面的示例中我们使用EnumMap来存储星期几对应的任务。通过直接使用枚举值作为键我们可以更清晰地表示和访问数据。
总之根据《Effective Java》的建议使用EnumMap代替序数索引可以提供更好的类型安全性、可读性和性能。它是实现基于枚举的数据结构的首选方式。
第38条用接口模拟可扩展的枚举
介绍了一种使用接口来模拟可扩展的枚举的技术。这种技术可以在不修改现有代码的情况下通过实现接口来扩展枚举类型。
通常情况下枚举类型是不可扩展的即不能在运行时动态添加新的枚举值。但是通过使用接口我们可以模拟出可扩展的枚举的行为。
下面是使用接口模拟可扩展的枚举的示例
首先定义一个表示枚举值的接口
public interface Operation {double apply(double x, double y);
}然后实现该接口的枚举类
public enum BasicOperation implements Operation {PLUS() {public double apply(double x, double y) { return x y; }},MINUS(-) {public double apply(double x, double y) { return x - y; }},MULTIPLY(*) {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;BasicOperation(String symbol) {this.symbol symbol;}public String toString() {return symbol;}
}在这个例子中BasicOperation是一个枚举类实现了Operation接口。每个枚举值都实现了apply方法用于执行相应的操作。
现在如果我们想要扩展这个枚举类型只需要实现Operation接口即可
public enum ExtendedOperation implements Operation {POWER(^) {public double apply(double x, double y) { return Math.pow(x, y); }},REMAINDER(%) {public double apply(double x, double y) { return x % y; }};private final String symbol;ExtendedOperation(String symbol) {this.symbol symbol;}public String toString() {return symbol;}
}通过实现Operation接口我们可以在不修改BasicOperation枚举类的情况下扩展枚举类型并添加新的操作。
使用这种技术我们可以实现可扩展的枚举同时保持类型安全性和可读性。
下面是一个使用示例
public class Main {public static void main(String[] args) {double x 10.0;double y 5.0;Operation op1 BasicOperation.PLUS;System.out.println(op1.apply(x, y)); // 输出 15.0Operation op2 ExtendedOperation.POWER;System.out.println(op2.apply(x, y)); // 输出 100000.0}
}在这个示例中我们使用了BasicOperation和ExtendedOperation枚举类来执行不同的操作并输出结果。通过使用接口模拟可扩展的枚举我们可以方便地添加新的操作而不需要修改现有的代码。
第39条注解优先于命名模式
介绍了一种使用注解优于命名模式的技术。这种技术可以提供更加灵活和可读性更好的代码。
传统上我们使用命名模式来表示某些特殊的情况或属性。例如我们可能使用命名模式来表示某个方法是一个测试方法或者某个字段是一个非空字段。
然而使用命名模式存在一些问题。首先命名模式需要开发人员遵循一定的命名规范以确保代码的可读性和一致性。其次命名模式可能会导致代码冗余因为每个特殊情况都需要使用不同的命名来表示。
为了解决这些问题Java引入了注解机制。通过使用注解我们可以在代码中直接标记特殊情况或属性而不需要依赖命名规范。
下面是使用注解优于命名模式的示例
首先定义一个注解
public interface Test {
}然后使用注解标记测试方法
public class MyClass {Testpublic void testMethod() {// 测试方法的实现}
}在这个例子中我们使用Test注解来标记一个测试方法。这样我们就可以直观地知道哪些方法是测试方法而不需要依赖命名规范。
另外注解还可以带有参数以提供更多的信息。例如我们可以定义一个带有参数的注解来表示某个字段是一个非空字段
public interface NonNull {String message() default Field cannot be null;
}然后使用注解标记非空字段
public class MyClass {NonNull(message Name cannot be null)private String name;
}在这个例子中我们使用NonNull注解来标记一个非空字段并提供了一个默认的错误消息。这样我们可以在编译时或运行时检查字段的非空性并提供相应的错误消息。
通过使用注解我们可以提供更加灵活和可读性更好的代码。注解可以直观地标记特殊情况或属性而不需要依赖命名规范。另外注解还可以带有参数以提供更多的信息。这样我们可以在编译时或运行时对代码进行更加精确的检查和处理。
第40条坚持使用Override注解
建议坚持使用Override注解来标记覆盖重写父类方法的方法。这样可以提高代码的可读性和可维护性。
在Java中当我们重写父类的方法时通常会使用Override注解来标记这个方法。这个注解的作用是告诉编译器我们是有意覆盖了父类的方法如果父类的方法签名发生了变化或者不存在编译器会给出错误提示。
下面是使用Override注解的示例
public class Parent {public void printMessage() {System.out.println(Parent class);}
}public class Child extends Parent {Overridepublic void printMessage() {System.out.println(Child class);}
}在这个例子中Child类继承自Parent类并重写了printMessage方法。在Child类中我们使用Override注解来标记这个方法。这样编译器会在编译时检查是否正确地覆盖了父类的方法。
使用Override注解的好处有以下几点 提高代码的可读性通过使用Override注解我们可以清楚地知道哪些方法是重写了父类的方法而不需要查看父类的源代码。 提供编译时错误检查如果我们错误地重写了父类的方法或者父类的方法签名发生了变化编译器会给出错误提示帮助我们及早发现和修复问题。 防止意外覆盖有时候我们可能会意外地重写了父类的方法而不是想要覆盖。使用Override注解可以帮助我们避免这种情况的发生。
总之坚持使用Override注解可以提高代码的可读性和可维护性。它可以清楚地标记出重写了父类方法的方法并提供编译时错误检查帮助我们编写更加健壮的代码。
第41条用标记接口定义类型
建议使用标记接口Marker Interface来定义类型。标记接口是一种不包含任何方法的接口仅用于标记某个类属于特定的类型。
使用标记接口的主要目的是为了提供一种更加灵活和可读性更好的类型检查机制。通过使用标记接口我们可以在编译时或运行时对对象进行类型检查以确定其是否属于特定的类型。
下面是使用标记接口的示例 public interface Serializable {// 空接口不包含任何方法
}public class Person implements Serializable {private String name;private int age;// 省略构造方法和其他方法// ...
}在这个例子中我们定义了一个名为Serializable的标记接口。然后我们将Person类实现了Serializable接口表示Person类是可序列化的。
使用标记接口的好处有以下几点 提供更加灵活的类型检查通过使用标记接口我们可以在编译时或运行时对对象进行类型检查以确定其是否属于特定的类型。这样可以提供更加灵活的类型检查机制而不仅仅局限于继承关系。 提高代码的可读性通过使用标记接口我们可以清楚地知道某个类属于特定的类型而不需要查看类的实现细节。这样可以提高代码的可读性和可维护性。 支持扩展性标记接口可以用于定义一组相关的类型而不仅仅是单个类型。这样可以支持更加灵活的扩展性可以根据需要定义新的标记接口来表示新的类型。
需要注意的是使用标记接口可能会增加代码的复杂性因为我们需要在适当的地方进行类型检查。因此在使用标记接口时需要权衡使用的场景和复杂性。
总之使用标记接口可以提供一种更加灵活和可读性更好的类型检查机制。它可以在编译时或运行时对对象进行类型检查以确定其是否属于特定的类型。通过使用标记接口我们可以提高代码的可读性和可维护性并支持更加灵活的扩展性。
第七章 Lambda 和 Stream
在Java 8中增加了函数接口、lambda和方法引用使用创建函数对象变得更容易。与此同时还增加了Stream API为处理数据元素的序列提供了类库级别的支持。在本章中将讨论如何最佳地利用这些机制。
第42条Lambda优先于匿名类
Lambda表达式是Java 8引入的一种简洁的语法用于表示函数式接口的实例。
Lambda表达式相比于匿名类具有以下优势 简洁性Lambda表达式可以大大减少代码的冗余使代码更加简洁易读。相比于匿名类Lambda表达式的语法更加简洁明了。 可读性Lambda表达式可以更好地表达代码的意图使代码更易于理解。通过使用Lambda表达式可以将代码的重点放在实现逻辑上而不是在类的声明和实例化上。 灵活性Lambda表达式可以更灵活地处理函数式接口。Lambda表达式可以直接传递给函数式接口的方法而不需要创建额外的类和实例。
下面是一个简单的示例展示了Lambda表达式和匿名类的对比
// 使用Lambda表达式
Runnable runnable1 () - System.out.println(Hello, Lambda!);// 使用匿名类
Runnable runnable2 new Runnable() {Overridepublic void run() {System.out.println(Hello, Anonymous Class!);}
};// 调用run方法
runnable1.run(); // 输出Hello, Lambda!
runnable2.run(); // 输出Hello, Anonymous Class!在上面的示例中我们创建了一个Runnable接口的实例。使用Lambda表达式我们可以直接通过() - System.out.println(“Hello, Lambda!”)来表示一个Runnable接口的实例。而使用匿名类则需要创建一个新的匿名类并实现其run方法。
通过使用Lambda表达式我们可以更简洁地表示函数式接口的实例并且代码更易读。因此在使用函数式接口时建议优先选择使用Lambda表达式而不是匿名类。
第43条方法引用优先于Lambda
优先选择使用方法引用而不是Lambda表达式。方法引用是一种更简洁的语法用于直接引用已经存在的方法。
方法引用相比于Lambda表达式具有以下优势 简洁性方法引用可以进一步减少代码的冗余使代码更加简洁易读。相比于Lambda表达式方法引用的语法更加简洁明了。 可读性方法引用可以更好地表达代码的意图使代码更易于理解。通过使用方法引用可以直接引用已经存在的方法而不需要编写额外的逻辑。 灵活性方法引用可以更灵活地处理函数式接口。方法引用可以直接传递给函数式接口的方法而不需要编写额外的Lambda表达式。
下面是一个简单的示例展示了方法引用和Lambda表达式的对比
// 使用方法引用
ListString names1 Arrays.asList(Alice, Bob, Charlie);
names1.forEach(System.out::println);// 使用Lambda表达式
ListString names2 Arrays.asList(Alice, Bob, Charlie);
names2.forEach(name - System.out.println(name));// 使用方法引用
ListString names Arrays.asList(Alice, Bob, Charlie);
names.forEach(String::toUpperCase);// 使用Lambda表达式
ListString names Arrays.asList(Alice, Bob, Charlie);
names.forEach(name - name.toUpperCase());// 使用方法引用
ListString names Arrays.asList(Alice, Bob, Charlie);
ListPerson persons names.stream().map(Person::new).collect(Collectors.toList());// 使用Lambda表达式
ListString names Arrays.asList(Alice, Bob, Charlie);
ListPerson persons names.stream().map(name - new Person(name)).collect(Collectors.toList());在上面的示例中我们使用了一个List的forEach方法来遍历列表中的元素并打印出来。使用方法引用我们可以直接通过System.out::println来引用System.out对象的println方法。而使用Lambda表达式则需要编写一个Lambda表达式来实现打印逻辑。
通过使用方法引用我们可以更简洁地表示已经存在的方法的引用并且代码更易读。因此在使用Lambda表达式时建议优先选择使用方法引用而不是Lambda表达式。
第44条坚持使用标准的函数接口
标准的函数接口是指Java标准库中已经定义好的函数接口例如java.util.function包中的接口。
使用标准的函数接口有以下好处 可读性标准的函数接口具有明确的命名和用途可以更好地表达代码的意图使代码更易于理解。 重用性标准的函数接口已经在Java标准库中定义好了可以直接使用无需自己定义新的接口。这样可以提高代码的重用性减少重复的工作。 互操作性标准的函数接口可以与其他库和框架进行良好的互操作。许多库和框架都已经支持标准的函数接口因此使用标准的函数接口可以更方便地与这些库和框架进行集成。
以下是一些使用标准的函数接口的示例
ListString names Arrays.asList(Alice, Bob, Charlie);
ListString filteredNames names.stream().filter(name - name.length() 4).collect(Collectors.toList());ListString names Arrays.asList(Alice, Bob, Charlie);
ListInteger nameLengths names.stream().map(name - name.length()).collect(Collectors.toList());ListString names Arrays.asList(Alice, Bob, Charlie);
names.forEach(name - System.out.println(name));这些示例展示了在不同情况下使用标准的函数接口的方式。使用标准的函数接口可以使代码更加清晰、可读并且具有良好的互操作性。因此在编写函数接口时建议优先选择使用标准的函数接口。
第45条谨慎使用Stream
Stream是Java 8引入的一个强大的API用于处理集合数据的流式操作。然而虽然Stream提供了很多便利的方法但过度使用Stream可能会导致代码变得复杂、难以理解和维护。
以下是书中详细说明Stream使用的几个方面 可读性Stream提供了一种流式的编程风格可以将多个操作链接在一起形成一个流水线。这种风格可以使代码更加简洁和可读。然而当流水线中的操作过多或过于复杂时代码可能会变得难以理解。因此在使用Stream时需要注意保持代码的可读性。 性能Stream的操作是延迟执行的只有在终止操作时才会触发实际的计算。这种延迟执行的特性可以提高性能避免不必要的计算。然而有时候过度使用Stream可能会导致性能下降。例如在某些情况下使用传统的循环可能比使用Stream更高效。因此在使用Stream时需要根据具体情况进行评估和权衡。 可变性Stream是不可变的它不会修改原始数据。这种不可变性可以确保数据的安全性和线程安全性。然而有时候需要对数据进行修改或更新这时候使用Stream可能会变得不太方便。因此在需要修改数据的场景下需要谨慎使用Stream。
以下是一个使用Stream的示例
ListString names Arrays.asList(Alice, Bob, Charlie);// 使用Stream过滤出长度大于4的字符串并将结果收集到一个新的List中
ListString filteredNames names.stream().filter(name - name.length() 4).collect(Collectors.toList());然而需要注意的是当流水线中的操作过多或过于复杂时代码可能会变得难以理解。因此在使用Stream时需要根据具体情况进行评估和权衡确保代码的可读性和性能。
总之Stream是一个强大的API可以提高代码的简洁性和可读性。然而过度使用Stream可能会导致代码变得复杂、难以理解和维护。因此在使用Stream时需要谨慎权衡并根据具体情况进行评估。
第46条优先选择Stream中无副作用的函数
副作用是指函数对除了返回值之外的其他状态进行了修改或产生了其他可观察的行为。在使用Stream时应该尽量避免使用具有副作用的函数而是优先选择无副作用的函数。
以下是书中详细说明无副作用函数的几个方面 可读性无副作用的函数更容易理解和推理。由于它们不会修改状态或产生其他可观察的行为所以可以更好地理解函数的行为和结果。 可测试性无副作用的函数更容易进行单元测试。由于它们不依赖于外部状态或其他因素所以可以更方便地编写和执行测试用例。 可组合性无副作用的函数更容易进行组合和重用。由于它们不会修改状态所以可以更方便地将它们组合在一起形成更复杂的操作。
以下是一个使用无副作用函数的示例
ListString names Arrays.asList(Alice, Bob, Charlie);// 使用无副作用的函数进行转换和过滤操作
ListString filteredNames names.stream().map(String::toUpperCase).filter(name - name.length() 4).collect(Collectors.toList());在上面的示例中我们使用无副作用的函数对一个字符串列表进行转换和过滤操作。首先我们使用map函数将所有字符串转换为大写形式然后使用filter函数过滤出长度大于4的字符串并最终将结果收集到一个新的List中。这个示例展示了无副作用函数的可读性、可测试性和可组合性。
需要注意的是有些函数可能会具有副作用例如forEach函数它可以对每个元素执行一个操作。在使用这些具有副作用的函数时需要谨慎评估和权衡并确保它们的使用不会导致意外的行为或不良的影响。
总之优先选择Stream中无副作用的函数可以提高代码的可读性、可测试性和可组合性。在使用具有副作用的函数时需要谨慎评估和权衡并确保它们的使用不会产生意外的行为。
第47条Stream要优先用Collection作为返回类型
这是因为Collection是Java中常见的集合类型使用Collection作为返回类型可以提供更好的互操作性和兼容性。
以下是书中详细说明使用Collection作为返回类型的几个方面 互操作性使用Collection作为返回类型可以方便地与其他集合类型进行互操作。由于Stream是Java 8引入的新特性不是所有的代码库和框架都对Stream提供了良好的支持。而使用Collection作为返回类型可以更容易地将Stream转换为其他集合类型或者将其他集合类型转换为Stream。 兼容性使用Collection作为返回类型可以提供更好的兼容性。在Java中Collection是一个广泛使用的接口几乎所有的集合类都实现了Collection接口。因此使用Collection作为返回类型可以使代码更加通用和灵活可以适应不同的集合实现。
以下是一个使用Collection作为返回类型的示例
public CollectionString getNames() {ListString names Arrays.asList(Alice, Bob, Charlie);return names.stream().filter(name - name.length() 4).collect(Collectors.toList());
}// 转换为List
ListString list collection.stream().collect(Collectors.toList());
// 转换为Set
SetString set collection.stream().collect(Collectors.toSet());
// 转换为数组
String[] array collection.stream().toArray(String[]::new);在上面的示例中我们定义了一个getNames方法该方法返回一个Collection类型的结果。在方法内部我们使用Stream对一个字符串列表进行过滤操作并最终将结果收集到一个新的List中。由于返回类型是Collection所以调用者可以根据需要将结果转换为其他集合类型例如Set或ArrayList。
需要注意的是如果返回类型是具体的集合类型例如List或Set那么调用者只能得到该具体类型的集合而无法灵活地将结果转换为其他集合类型。而使用Collection作为返回类型可以提供更好的灵活性和兼容性。
总之使用Collection作为Stream的返回类型可以提供更好的互操作性和兼容性。这样可以使代码更加通用和灵活适应不同的集合实现。
第48条谨慎使用Stream并行
因为并行操作可能会引发线程安全问题和性能问题。
以下是书中详细说明使用Stream并行的几个方面 线程安全问题并行操作会将数据分成多个部分并使用多个线程同时处理这些部分。如果在并行操作中修改了共享的数据可能会导致线程安全问题。因此在使用并行操作时需要确保操作是无状态的或线程安全的。 性能问题并行操作需要将数据分成多个部分并创建多个线程来处理这些部分。这样会增加线程调度和数据传输的开销可能导致性能下降。在某些情况下串行操作可能比并行操作更快。
因此书中建议在使用Stream并行时需要谨慎并在以下情况下考虑使用并行操作
数据量大且操作是无状态的或线程安全的。操作是计算密集型的而不是I/O密集型的。经过测试证明并行操作确实能够提高性能。
以下是一个使用Stream并行的示例
ListInteger numbers Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);int sum numbers.parallelStream().filter(n - n % 2 0).mapToInt(n - n).sum();System.out.println(sum);在上述示例中我们使用parallelStream()方法将Stream转换为并行流并对数字进行过滤和求和操作。由于这个示例中的操作是无状态的并且计算密集型因此使用并行操作可以提高性能。但是需要注意如果操作涉及到共享数据或是I/O密集型的操作就需要谨慎使用并行操作。
第八章 方法
本章要讨论方法设计的几个方面如何处理参数和返回值如何设计方法签名如何为方法编写文档。本章大部分内容既适用于构造器也适用于普通的方法。与第4章一样本章的焦点也集中在可用性、健壮性和灵活性上。
第49条检查参数的有效性
建议了在方法中检查参数的有效性以确保方法的正确性和健壮性。
以下是书中详细说明参数有效性检查的几个方面 检查参数是否为null在方法中检查参数是否为null是一种良好的编程习惯可以避免空指针异常。如果参数为null可以抛出NullPointerException或者提前返回错误结果。 检查参数的取值范围对于有限的取值范围的参数可以在方法中检查参数的取值是否合法。如果参数的取值不在合法范围内可以抛出IllegalArgumentException或者提前返回错误结果。 检查参数之间的关联性有些方法的参数之间可能存在关联性需要检查这些参数的关联性是否满足要求。如果参数之间的关联性不满足要求可以抛出IllegalArgumentException或者提前返回错误结果。
以下是一个示例演示了如何在方法中检查参数的有效性
public void processOrder(String orderId, int quantity) {if (orderId null) {throw new NullPointerException(orderId cannot be null);}if (quantity 0) {throw new IllegalArgumentException(quantity must be positive);}// 执行订单处理逻辑// ...
}在上述示例中processOrder方法接收一个订单ID和数量作为参数。在方法中首先检查订单ID是否为null如果为null则抛出NullPointerException。然后检查数量是否小于等于0如果小于等于0则抛出IllegalArgumentException。通过这些参数有效性检查可以确保方法在执行之前参数的有效性提高方法的健壮性。
需要注意的是参数有效性检查应该在方法的开头进行以便尽早发现错误并提前返回。同时还可以使用断言assert来进行参数有效性检查但是断言默认是关闭的需要在运行时启用。
第50条必要时进行保护性拷贝
建议了在需要时进行保护性拷贝Defensive Copy以确保不可变性和安全性。
保护性拷贝是指在对外部提供访问对象的方法时返回一个拷贝而不是原始对象以防止外部修改对象的状态。这样可以保持对象的不可变性和安全性。
以下是书中详细说明保护性拷贝的几个方面 不可变性如果一个类的实例是不可变的那么在对外部提供访问该实例的方法时应该返回一个拷贝而不是原始实例。这样可以防止外部修改原始实例的数据从而保持不可变性。 安全性如果一个类的实例包含可变的成员变量并且这些成员变量对外部是可见的那么在对外部提供访问这些成员变量的方法时应该返回一个拷贝而不是原始实例。这样可以防止外部修改原始实例的成员变量从而保证安全性。
以下是一个示例演示了如何进行保护性拷贝
public class Person {private final String name;private final Date birthDate;public Person(String name, Date birthDate) {this.name name;this.birthDate new Date(birthDate.getTime()); // 进行保护性拷贝}public String getName() {return name;}public Date getBirthDate() {return new Date(birthDate.getTime()); // 进行保护性拷贝}
}在上述示例中Person类包含一个不可变的name属性和一个可变的birthDate属性。在构造方法中对传入的birthDate进行了保护性拷贝使用new Date(birthDate.getTime())创建了一个新的Date对象。在getBirthDate方法中也进行了保护性拷贝返回了一个新的Date对象。这样外部无法修改Person实例的birthDate属性保证了不可变性和安全性。
需要注意的是进行保护性拷贝时需要根据具体情况选择合适的方式进行拷贝。对于不可变的对象可以直接返回原始实例或者返回一个拷贝。对于可变的对象应该返回一个拷贝以防止外部修改原始实例的数据。保护性拷贝的目的是为了保护对象的状态确保对象在外部使用时不会被修改。
第51条谨慎设计方法签名
建议了谨慎设计方法签名Carefully Design Method Signatures以提高代码的可读性和灵活性。
方法签名是指方法的名称、参数列表和返回类型。一个好的方法签名应该清晰地表达方法的功能和用途并且应该尽量避免使用具有歧义的参数类型或返回类型。
以下是书中详细说明谨慎设计方法签名的几个方面 使用明确的命名方法名应该能够清晰地表达方法的功能和用途。避免使用模糊或不相关的名称以免给其他开发人员造成困惑。 使用具体的参数类型在方法的参数列表中应该尽量使用具体的类型而不是抽象类型或接口。这样可以提高代码的可读性并且可以避免在方法内部进行类型转换的操作。 避免使用过多的参数方法的参数列表应该尽量简洁避免使用过多的参数。过多的参数会增加方法的复杂性并且容易引发错误。如果方法需要接收大量的参数可以考虑使用构建器模式或者将参数封装成一个对象。 考虑使用重载方法如果一个类中有多个方法具有相似的功能但是参数类型不同可以考虑使用重载方法。重载方法可以提高代码的可读性并且可以根据不同的参数类型选择合适的方法进行调用。
下面是一个示例演示了如何谨慎设计方法签名
public class Calculator {public int add(int a, int b) {return a b;}public double add(double a, double b) {return a b;}public int multiply(int a, int b) {return a * b;}public double multiply(double a, double b) {return a * b;}
}在上面的示例中Calculator类中定义了两个add方法和两个multiply方法。这些方法具有相似的功能但是参数类型不同。通过使用重载方法可以根据不同的参数类型选择合适的方法进行调用提高了代码的可读性和灵活性。
第52条慎用重载
重载是指在同一个类中定义多个具有相同名称但参数列表不同的方法。重载可以提供更多的灵活性和方便性但过度使用重载可能会导致代码难以理解和维护。
该条建议的详细说明如下 避免混淆当存在多个重载方法时调用者可能会因为参数类型的相似性而产生混淆导致选择错误的方法。这会增加代码的复杂性和错误的可能性。 可读性和可维护性过多的重载方法会增加代码的复杂性使代码难以理解和维护。当代码中存在多个重载方法时阅读代码的人需要仔细查看每个方法的参数类型和返回类型才能确定调用的是哪个方法。 重载与重写的混淆当一个类中同时存在重载方法和重写方法时容易混淆两者的概念。重载是指在同一个类中定义多个具有相同名称但参数列表不同的方法而重写是指子类覆盖父类中的方法。混淆两者可能导致意外的行为和错误。
下面是一个正例和反例来说明慎用重载的原因
正例
public class Calculator {public int add(int a, int b) {return a b;}public double add(double a, double b) {return a b;}
}在上面的示例中Calculator 类定义了两个重载的 add 方法分别接受两个整数和两个浮点数作为参数并返回它们的和。这种情况下重载方法的使用是合理的因为参数类型不同调用者可以根据需要选择正确的方法。
反例
public class Calculator {public int add(int a, int b) {return a b;}public int add(int a, int b, int c) {return a b c;}
}在上面的示例中Calculator 类定义了两个重载的 add 方法分别接受两个整数和三个整数作为参数并返回它们的和。这种情况下重载方法的使用可能会导致混淆和错误。调用者在使用 add 方法时如果传递了三个整数的参数可能会错误地调用了第二个重载方法而不是期望的第一个重载方法。
为了避免重载带来的混淆和复杂性可以考虑使用不同的方法名或使用不同的参数类型来区分功能相似但参数不同的方法。这样可以提高代码的可读性和可维护性。
第53条慎用可变参数
可变参数Varargs是Java中的一种特殊语法它允许方法接受任意数量的参数。在方法声明中使用省略号…来表示可变参数。可变参数实际上是一个数组方法内部可以像操作数组一样来处理这些参数。
该条建议的详细说明如下 可变参数的使用应该谨慎。可变参数允许方法接受任意数量的参数但这种灵活性可能会导致一些问题。 可变参数的主要问题是类型安全性。可变参数是通过数组来实现的因此在编译时无法对参数类型进行检查。这意味着在运行时如果传递了错误类型的参数可能会导致运行时异常。 可变参数还会导致一些模糊的调用。当方法有多个重载版本时如果传递的参数数量和类型与多个重载方法匹配编译器可能会选择错误的方法。
下面是一个例子来说明慎用可变参数的原因
public class Calculator {public static int sum(int... numbers) {int sum 0;for (int number : numbers) {sum number;}return sum;}
}在上面的示例中Calculator 类定义了一个可变参数的 sum 方法用于计算传入参数的总和。这种情况下可变参数的使用是合理的因为它提供了一种方便的方式来接受任意数量的参数。
然而可变参数的使用也可能导致一些问题。考虑以下示例
public class Calculator {public static int sum(int... numbers) {int sum 0;for (int number : numbers) {sum number;}return sum;}public static int sum(int a, int b) {return a b;}
}在上面的示例中Calculator 类定义了两个重载的 sum 方法一个是可变参数的版本另一个是接受两个整数的版本。如果调用者传递两个整数作为参数编译器可能会选择错误的方法因为可变参数的版本也可以接受两个整数作为参数。
为了避免可变参数带来的类型安全性和调用模糊性的问题可以考虑使用重载方法或明确指定参数类型来替代可变参数。这样可以提高代码的可读性和可维护性。
第54条返回零长度的数组或者集合、而不是null
该条建议的详细说明如下 返回零长度的数组或者集合比返回null更好。返回null可能会导致调用者需要额外的空指针检查增加代码的复杂性和出错的可能性。 返回零长度的数组或者集合可以简化调用者的代码逻辑。调用者可以直接使用返回的空数组或者集合而不需要进行额外的判断和处理。 返回零长度的数组或者集合可以提供更好的API一致性。如果一个方法在某些情况下返回null在其他情况下返回非null的数组或者集合会导致调用者需要编写不同的处理逻辑增加了使用的复杂性。
下面是一个示例来说明返回零长度的数组或者集合的使用
public class StringUtils {public static String[] split(String input, String delimiter) {if (input null || input.isEmpty()) {return new String[0];}return input.split(delimiter);}
}在上面的示例中split 方法用于将字符串按照指定的分隔符进行拆分。如果输入字符串为空或者null方法会返回一个零长度的字符串数组。这样调用者可以直接使用返回的数组而不需要进行额外的空指针检查。
调用者可以按照以下方式使用 split 方法
String[] result1 StringUtils.split(Hello,World, ,); // result1 [Hello, World]
String[] result2 StringUtils.split(, ,); // result2 []在上面的示例中无论输入字符串是非空还是空split 方法都会返回一个合法的字符串数组。这样调用者可以直接使用返回的数组而不需要担心空指针异常。
总结起来返回零长度的数组或者集合比返回null更好。它可以简化调用者的代码逻辑提供更好的API一致性并减少空指针异常的可能性。在设计方法时应该考虑返回零长度的数组或者集合而不是null。
第55条谨慎返回optional
该条建议的详细说明如下 Optional是Java 8引入的一个用于表示可能为空的值的容器类。它可以用于替代返回null的情况提供更好的语义和错误处理机制。 虽然Optional可以提供更好的可读性和错误处理但并不是所有情况下都适合使用Optional。过度使用Optional可能会导致代码变得复杂增加了调用者的负担。 应该谨慎使用Optional只在以下情况下使用 1. 方法的返回值可能为空但是调用者需要明确处理这种情况。 2. 方法的返回值可能为空但是调用者可以使用默认值或者采取其他合适的处理方式。
下面是一个示例来说明谨慎使用Optional的情况
import java.util.Optional;public class Person {private String name;private OptionalInteger age;public Person(String name, OptionalInteger age) {this.name name;this.age age;}public OptionalInteger getAge() {return age;}public static void main(String[] args) {Person person1 new Person(John, Optional.of(25));Person person2 new Person(Jane, Optional.empty());int age1 person1.getAge().orElse(0);int age2 person2.getAge().orElse(0);System.out.println(Person 1 age: age1); // 输出Person 1 age: 25System.out.println(Person 2 age: age2); // 输出Person 2 age: 0}
}在上面的示例中Person类有一个age字段类型为Optional表示年龄信息。在构造方法中我们可以传入一个年龄值或者空值。
在main方法中我们创建了两个Person对象分别是person1和person2。person1的年龄信息存在而person2的年龄信息为空。
我们使用getAge方法来获取人的年龄信息。如果年龄信息存在我们可以使用orElse方法来获取年龄值如果年龄信息为空我们可以使用orElse方法提供一个默认值。
在上面的示例中person1的年龄信息存在所以age1的值为25person2的年龄信息为空所以age2的值为0。
通过使用Optional我们可以明确处理可能为空的值并且可以提供默认值或者其他合适的处理方式。这样可以使代码更加清晰和健壮。
谨慎使用Optional的原因有以下几点 增加复杂性使用Optional可能会增加代码的复杂性。在使用Optional的过程中需要使用一些特定的方法来处理Optional对象如orElse、orElseGet、orElseThrow等。这些方法的使用需要一定的学习成本并且可能会使代码变得更加冗长。 过度使用过度使用Optional可能会导致代码变得冗长和难以理解。Optional应该被用于表示可能为空的值而不是用于替代所有的null检查。如果在每个可能为空的地方都使用Optional会使代码变得过于冗长降低代码的可读性。 引入新的问题使用Optional可能会引入一些新的问题。例如如果在Optional中存储了null值那么在使用Optional的过程中可能会出现NullPointerException。此外如果在方法的返回类型中使用Optional可能会导致调用者需要处理Optional对象增加了调用者的负担。 兼容性问题Optional是在Java 8中引入的如果代码需要与旧版本的Java兼容那么使用Optional可能会导致兼容性问题。
综上所述虽然Optional可以提供更好的语义和错误处理机制但在使用Optional时需要谨慎考虑避免过度使用和增加代码的复杂性。在某些情况下使用传统的null检查可能更加简单和直观。
第56条为所有导出的API元素编写文档注释
该条建议的原因如下 提供清晰的文档编写文档注释可以提供清晰、准确的文档帮助其他开发人员理解和正确使用API。文档注释应该包含API的用途、参数的含义、返回值的含义以及可能抛出的异常等信息使其他开发人员能够快速了解API的使用方式和限制条件。 提高代码的可读性文档注释可以提高代码的可读性。通过文档注释其他开发人员可以更容易地理解代码的意图和设计思路从而更好地维护和扩展代码。 促进团队协作编写文档注释可以促进团队协作。通过清晰的文档注释团队成员可以更好地理解和使用彼此编写的代码减少沟通成本提高开发效率。 支持自动生成文档文档注释可以支持自动生成API文档。许多开发工具和框架都支持从代码中提取文档注释并生成API文档这样可以节省编写文档的时间和精力。
下面是一个示例展示了如何为一个导出的API元素编写文档注释
/*** 计算两个整数的和。** param a 第一个整数* param b 第二个整数* return 两个整数的和*/
public int add(int a, int b) {return a b;
}在上面的示例中文档注释清楚地说明了方法的用途、参数的含义和返回值的含义。其他开发人员可以通过阅读文档注释快速了解该方法的使用方式和预期结果。
第九章 通用编程
本章主要讨论Java语言的细枝末节包含局部变量的处理、控制结构、类库的用法、各种数据类型的用法以及两种不是由语言本身提供的机制反射机制和本地方法的用法。最后讨论了优化和命名规则。
第57条将局部变量的作用域最小化
将局部变量的作用域最小化这是为了提高代码的可读性、可维护性和安全性。通过将局部变量的作用域限制在尽可能小的范围内可以减少变量的生命周期避免变量被误用或意外修改同时也可以减少内存占用。
具体来说将局部变量的作用域最小化可以遵循以下几个原则 在第一次使用变量的地方声明它在需要使用变量的地方直接声明而不是在方法的开头或其他不必要的地方声明。这样可以使代码更加清晰读者可以更容易地理解变量的用途和范围。 尽量延迟变量的声明只有在需要使用变量之前才进行声明而不是在方法的开头或其他不必要的地方。这样可以减少变量的生命周期避免变量被误用或意外修改。 尽量使用final修饰符对于不需要修改的变量可以使用final修饰符来明确表示其不可变性。这样可以提高代码的可读性并且编译器可以进行更多的优化。
下面是一个示例代码演示了如何将局部变量的作用域最小化
public void processOrder(Order order) {// 声明并初始化变量int quantity order.getQuantity();// 使用变量进行计算double totalPrice calculateTotalPrice(quantity, order.getPrice());// 打印结果System.out.println(Total price: totalPrice);
}private double calculateTotalPrice(int quantity, double price) {// 在方法内部声明变量double discount 0.1;// 使用变量进行计算double discountedPrice price * (1 - discount);double totalPrice discountedPrice * quantity;return totalPrice;
}在上面的示例中变量quantity、totalPrice和discount的作用域都被最小化到了需要使用它们的地方。这样可以使代码更加清晰读者可以更容易地理解变量的用途和范围。同时变量的生命周期也被限制在了需要使用它们的方法内部避免了变量被误用或意外修改的风险。
第58条for-each循环优先于传统的for循环
建议在遍历集合或数组时优先使用for-each循环而不是传统的for循环。使用for-each循环可以使代码更加简洁、易读并且可以减少出错的可能性。
传统的for循环需要手动管理索引和长度并且需要使用索引来访问集合或数组中的元素。这样容易出现索引越界错误或者忘记更新索引的情况。而for-each循环则不需要手动管理索引它会自动迭代集合或数组中的每个元素使代码更加简洁和易读。
具体来说使用for-each循环有以下几个优点 简洁for-each循环可以将遍历集合或数组的代码简化为一行不需要手动管理索引和长度。 安全for-each循环在编译时会进行类型检查可以避免类型不匹配的错误。 可读性for-each循环可以使代码更加清晰易读不需要关注索引和长度的细节。
下面是一个示例代码演示了如何使用for-each循环和传统的for循环遍历数组
public void printArray(int[] array) {// 使用for-each循环遍历数组for (int num : array) {System.out.println(num);}// 使用传统的for循环遍历数组for (int i 0; i array.length; i) {System.out.println(array[i]);}
}在上面的示例中使用for-each循环可以将遍历数组的代码简化为一行不需要手动管理索引和长度。而传统的for循环需要使用索引来访问数组中的元素并且需要手动管理索引和长度。使用for-each循环可以使代码更加简洁、易读并且可以减少出错的可能性。
第59条了解和使用类库
建议了解和使用类库这是为了避免重复造轮子提高开发效率和代码质量。Java类库提供了丰富的功能和工具可以帮助开发人员解决常见的问题减少开发工作量并且经过了广泛的测试和优化具有较高的可靠性和性能。
具体来说了解和使用类库可以遵循以下几个原则 熟悉常用的类库了解Java标准库中常用的类和方法例如集合框架、IO操作、日期时间处理、正则表达式等。这些类库提供了常见问题的解决方案可以减少开发人员的工作量。 使用第三方类库除了Java标准库还有许多优秀的第三方类库可供使用。这些类库提供了更丰富的功能和更高级的特性可以帮助开发人员更快地实现复杂的功能。例如Apache Commons提供了许多常用的工具类Google Guava提供了更强大的集合类Jackson提供了JSON处理功能等。 避免重复造轮子在开发过程中遇到常见的问题时先查看是否有现成的类库可以使用。避免重复实现已经存在的功能可以节省开发时间并且可以使用经过测试和优化的类库提高代码的质量和可靠性。
下面是一个示例代码演示了如何使用Java标准库中的类库和第三方类库
import java.util.ArrayList;
import java.util.List;public class LibraryExample {public static void main(String[] args) {// 使用Java标准库中的ArrayList类ListString list new ArrayList();list.add(Java);list.add(Library);System.out.println(list);// 使用第三方类库Apache Commons中的StringUtils类String str Hello World ;String trimmedStr org.apache.commons.lang3.StringUtils.trim(str);System.out.println(trimmedStr);}
}在上面的示例中使用了Java标准库中的ArrayList类来创建一个列表并使用add方法添加元素。同时使用了第三方类库Apache Commons中的StringUtils类来去除字符串两端的空格。通过使用类库可以简化开发过程提高开发效率和代码质量。
第60条如果需要精确的答案请避免使用float和double
建议在需要精确答案的情况下避免使用float和double类型。float和double是Java中表示浮点数的数据类型它们可以表示非常大或非常小的数值范围但是由于浮点数的内部表示方式的限制它们无法精确地表示所有的数值。
浮点数的内部表示方式采用了二进制的科学计数法即使用一个小数点和指数来表示一个数值。由于二进制无法精确地表示某些十进制数因此在进行浮点数计算时可能会出现舍入误差和精度丢失的问题。
具体来说使用float和double可能会导致以下问题 舍入误差由于浮点数的内部表示方式的限制进行浮点数计算时可能会出现舍入误差。例如对于0.1这个十进制数在float或double中无法精确表示因此进行计算时可能会出现舍入误差。 精度丢失由于浮点数的内部表示方式的限制某些十进制数在float或double中无法精确表示因此可能会丢失一些精度。例如对于0.1这个十进制数在float或double中无法精确表示因此可能会丢失一些小数位的精度。
为了避免这些问题如果需要精确答案可以使用BigDecimal类来进行精确计算。BigDecimal类可以表示任意精度的十进制数并且提供了精确的计算方法。
下面是一个示例代码演示了使用BigDecimal进行精确计算的例子
import java.math.BigDecimal;public class PrecisionExample {public static void main(String[] args) {BigDecimal num1 new BigDecimal(0.1);BigDecimal num2 new BigDecimal(0.2);BigDecimal sum num1.add(num2);System.out.println(sum); // 输出0.3精确计算结果BigDecimal product num1.multiply(num2);System.out.println(product); // 输出0.02精确计算结果}
}在上面的示例中使用BigDecimal类创建了两个精确的十进制数并使用add方法和multiply方法进行精确计算。通过使用BigDecimal类可以避免浮点数计算中的舍入误差和精度丢失问题得到精确的答案。
第61条基本类型优先于装箱基本类型
建议在可能的情况下优先使用基本类型而不是装箱基本类型。基本类型是Java中的原始数据类型而装箱基本类型是对应的包装类用于将基本类型包装成对象。
使用基本类型而不是装箱基本类型可以带来以下几个好处 性能更好基本类型的操作通常比装箱基本类型更高效。因为装箱基本类型需要将基本类型转换为对象涉及到额外的内存分配和对象初始化的开销。而基本类型的操作直接在栈上进行不需要额外的内存分配和对象初始化。 内存占用更小基本类型占用的内存空间通常比装箱基本类型更小。装箱基本类型需要额外的对象头和引用字段而基本类型只需要存储对应的数值。 避免空指针异常装箱基本类型可以为null而基本类型不可以。如果使用装箱基本类型时没有进行null检查可能会导致空指针异常。
下面是一个示例代码演示了使用基本类型和装箱基本类型的区别
public class BoxingExample {public static void main(String[] args) {int primitive 10;Integer boxed 10;// 基本类型的操作int result1 primitive 5;System.out.println(result1); // 输出15// 装箱基本类型的操作Integer result2 boxed 5;System.out.println(result2); // 输出15// 装箱基本类型可能导致空指针异常Integer nullBoxed null;int result3 nullBoxed 5; // 抛出NullPointerException}
}在上面的示例中使用基本类型int和装箱基本类型Integer进行了加法操作。可以看到基本类型的操作直接在栈上进行而装箱基本类型的操作需要将Integer对象拆箱为int类型进行计算。此外如果装箱基本类型为null进行操作时会抛出空指针异常。
因此根据第61条的建议在可能的情况下应优先使用基本类型而不是装箱基本类型以获得更好的性能和内存占用并避免空指针异常。只有在需要使用对象的特性时才使用装箱基本类型。
第62条如果其他类型更适合则尽量避免使用字符串
建议在其他类型更适合的情况下尽量避免使用字符串。虽然字符串是Java中常用的数据类型之一但是在某些情况下使用其他类型可以提供更好的性能、可读性和安全性。
以下是一些使用其他类型替代字符串的情况
枚举类型如果需要表示一组固定的值且这些值是预定义的可以使用枚举类型来替代字符串。枚举类型提供了类型安全性和可读性同时也可以方便地进行比较和遍历。
enum Day {MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}Day today Day.MONDAY;数值类型如果需要进行数值计算或比较使用数值类型如int、double等比使用字符串更高效。数值类型可以直接进行算术运算和比较操作而字符串需要进行解析和转换。
int count 10;
double price 19.99;类型安全的类如果需要表示特定的数据类型可以使用自定义的类型安全的类来替代字符串。这样可以提供更好的类型检查和可读性。
class EmailAddress {private String value;public EmailAddress(String value) {// 验证邮箱地址的格式if (!isValidEmailAddress(value)) {throw new IllegalArgumentException(Invalid email address);}this.value value;}// 其他方法...
}EmailAddress email new EmailAddress(exampleexample.com);集合类型如果需要存储一组元素并进行增删查改操作使用集合类型如List、Set等比使用字符串更方便和高效。
ListString names new ArrayList();
names.add(Alice);
names.add(Bob);
names.add(Charlie);总之根据第62条的建议在其他类型更适合的情况下尽量避免使用字符串。选择合适的数据类型可以提供更好的性能、可读性和安全性。只有在需要处理文本内容或字符串操作时才使用字符串。
第63条了解字符串连接的性能
建议了解字符串连接的性能并选择合适的方法进行字符串连接操作。字符串连接是在Java中常见的操作但是不同的连接方法会对性能产生不同的影响。
在Java中有以下几种字符串连接的方法
使用“操作符使用”“操作符进行字符串连接是最简单直观的方法但是它的性能较差。每次使用”操作符连接字符串时都会创建一个新的String对象导致频繁的内存分配和对象复制。
String result Hello World;使用StringBuilder或StringBufferStringBuilder和StringBuffer是可变的字符串类它们提供了高效的字符串连接操作。StringBuilder是非线程安全的而StringBuffer是线程安全的。使用StringBuilder或StringBuffer的append方法可以避免频繁的内存分配和对象复制。
StringBuilder sb new StringBuilder();
sb.append(Hello);
sb.append( );
sb.append(World);
String result sb.toString();使用String.join方法Java 8引入了String类的join方法可以更方便地进行字符串连接操作。它接受一个分隔符和一个字符串数组或集合将数组中的元素用分隔符连接起来。
String[] words {Hello, World};
String result String.join( , words);了解字符串连接的性能可以帮助我们选择合适的方法。在大量字符串连接的场景中使用StringBuilder或StringBuffer比使用“操作符更高效。而在连接固定数量的字符串时使用”操作符可能更简洁明了。
总之根据第63条的建议了解字符串连接的性能并选择合适的方法进行字符串连接操作可以提高程序的性能和效率。
第64条通过接口引用对象
建议使用接口类型来引用对象而不是使用具体的实现类。这样做可以提高代码的灵活性和可扩展性使代码更易于维护和修改。
使用接口类型引用对象的好处有以下几点 降低耦合性通过使用接口类型引用对象可以将代码与具体的实现类解耦。这意味着可以在不修改现有代码的情况下轻松地替换实现类或添加新的实现类。 提高可扩展性使用接口类型引用对象可以方便地添加新的实现类从而扩展系统的功能。这样可以遵循开闭原则即对扩展开放对修改关闭。 支持多态性通过接口类型引用对象可以实现多态性。这意味着可以在运行时根据实际对象的类型调用相应的方法而不需要在编译时确定具体的实现类。
下面是一个示例演示了如何使用接口类型引用对象
// 定义一个接口
interface Animal {void makeSound();
}// 实现接口的具体类
class Dog implements Animal {Overridepublic void makeSound() {System.out.println(Dog barks);}
}class Cat implements Animal {Overridepublic void makeSound() {System.out.println(Cat meows);}
}public class Main {public static void main(String[] args) {// 使用接口类型引用对象Animal animal1 new Dog();Animal animal2 new Cat();// 调用接口方法animal1.makeSound(); // 输出: Dog barksanimal2.makeSound(); // 输出: Cat meows}
}在上面的示例中Animal接口定义了一个makeSound()方法Dog和Cat类分别实现了该接口。在Main类中使用Animal接口类型引用了Dog和Cat对象然后调用了makeSound()方法。由于使用了接口类型引用对象可以根据实际对象的类型来调用相应的方法实现了多态性。
通过使用接口类型引用对象可以将代码与具体的实现类解耦提高代码的灵活性和可扩展性。这是一种良好的编程实践可以使代码更易于维护和修改。
第65条接口优先于反射机制
建议在可能的情况下优先使用接口而不是反射机制。接口是一种定义行为的契约可以提供更清晰、更可读、更可维护的代码。而反射机制则是一种动态获取和操作类的能力它可以在运行时通过类的名称来获取类的信息并进行操作。
使用接口而不是反射机制的好处有以下几点 易于理解和维护接口提供了一种清晰的契约定义了类应该具有的行为。通过使用接口可以更容易地理解代码的意图和功能。而反射机制则是一种动态的、隐式的方式来获取和操作类的信息容易使代码变得复杂和难以理解。 提高性能反射机制在运行时需要进行额外的检查和处理这会导致一定的性能损耗。而使用接口可以在编译时进行类型检查避免了运行时的额外开销提高了代码的性能。 编译时类型检查使用接口可以在编译时进行类型检查确保代码的类型安全性。而反射机制则是在运行时才能确定类的信息容易导致类型错误和运行时异常。
下面是一个示例演示了使用接口和反射机制的对比
// 定义一个接口
interface Animal {void makeSound();
}// 实现接口的具体类
class Dog implements Animal {Overridepublic void makeSound() {System.out.println(Dog barks);}
}public class Main {public static void main(String[] args) {// 使用接口Animal animal new Dog();animal.makeSound(); // 输出: Dog barks// 使用反射机制try {Class? clazz Class.forName(Dog);Animal animal2 (Animal) clazz.newInstance();animal2.makeSound(); // 输出: Dog barks} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {e.printStackTrace();}}
}在上面的示例中Animal接口定义了一个makeSound()方法Dog类实现了该接口。在Main类中首先使用接口类型引用了Dog对象并调用了makeSound()方法。接着使用反射机制通过类的名称获取了Dog类的信息并实例化了一个Dog对象然后调用了makeSound()方法。
通过对比可以看出使用接口的代码更加简洁、清晰易于理解和维护。而使用反射机制的代码则需要额外的异常处理和类型转换增加了代码的复杂性。
因此根据第65条的建议在可能的情况下应该优先使用接口而不是反射机制以提高代码的可读性、可维护性和性能。只有在必要的情况下才使用反射机制。
第66条谨慎地使用本地方法
谨慎地使用本地方法Native Methods。本地方法是指使用非Java语言如C、C编写的方法可以通过Java Native InterfaceJNI在Java程序中调用。
使用本地方法的好处有以下几点 提高性能本地方法可以直接调用底层系统的功能可以获得更高的执行效率。对于一些对性能要求较高的场景使用本地方法可以提升程序的运行速度。 访问底层资源本地方法可以访问一些Java无法直接访问的底层资源如操作系统的API、硬件设备等。通过使用本地方法可以扩展Java程序的功能和能力。
然而使用本地方法也存在一些潜在的问题和风险需要谨慎使用 可移植性问题本地方法依赖于底层系统的特定实现因此在不同的平台上可能存在不同的实现。这会导致程序在不同平台上的行为不一致降低了程序的可移植性。 安全性问题本地方法可以直接访问底层资源这可能导致安全漏洞。如果本地方法没有正确地处理输入数据或没有进行足够的安全检查可能会导致程序受到攻击。 调试和维护问题本地方法的调试和维护相对复杂需要熟悉底层语言和工具。如果本地方法出现问题可能需要使用底层语言的调试工具进行排查和修复。
下面是一个简单的示例演示了如何使用本地方法
public class NativeExample {// 声明本地方法private native void nativeMethod();// 加载本地库static {System.loadLibrary(nativeLibrary);}public static void main(String[] args) {NativeExample example new NativeExample();example.nativeMethod(); // 调用本地方法}
}在上面的示例中NativeExample类声明了一个本地方法nativeMethod()。在静态代码块中使用System.loadLibrary()方法加载了名为nativeLibrary的本地库。然后在main方法中创建了NativeExample对象并调用了nativeMethod()方法。
需要注意的是本地方法的实现是在外部的本地库中需要使用底层语言如C、C编写并通过JNI与Java程序进行交互。
总之使用本地方法可以提高性能和访问底层资源的能力但也存在一些潜在的问题和风险。在使用本地方法时需要谨慎考虑可移植性、安全性、调试和维护等方面的问题并确保正确地使用和管理本地方法。
第67条谨慎地进行优化
建议谨慎地进行优化Optimize judiciously。优化是指对代码进行改进以提高性能或减少资源消耗。虽然优化可以带来一些好处但过度优化可能会导致代码变得复杂、难以理解和维护并且可能无法带来明显的性能提升。
在进行优化时需要谨慎考虑以下几点 确定性能瓶颈在进行优化之前首先需要确定代码的性能瓶颈所在。通过使用性能分析工具可以找到代码中耗时的部分然后有针对性地进行优化。 优化可读性和可维护性在进行优化时需要权衡代码的可读性和可维护性。过度优化可能会导致代码变得复杂和难以理解从而增加了维护的难度。因此需要确保优化后的代码仍然具有良好的可读性和可维护性。 使用合适的数据结构和算法在优化代码时可以考虑使用更高效的数据结构和算法。例如使用哈希表代替线性搜索使用快速排序代替冒泡排序等。选择合适的数据结构和算法可以显著提高代码的性能。 避免过早优化过早优化是指在没有明确性能问题的情况下进行优化。在代码的早期阶段应该更关注代码的可读性、可维护性和正确性。只有在性能问题确实存在时才进行优化。
下面是一个简单的示例演示了如何进行优化
public class OptimizationExample {public static void main(String[] args) {ListInteger numbers new ArrayList();// 添加一百万个整数到列表中for (int i 0; i 1000000; i) {numbers.add(i);}// 计算列表中所有整数的和int sum 0;for (int number : numbers) {sum number;}System.out.println(Sum: sum);}
}在上面的示例中我们使用一个列表存储一百万个整数并计算列表中所有整数的和。这段代码的性能可能不够理想因为使用了一个简单的线性搜索来遍历列表。为了优化性能我们可以使用Java 8引入的流StreamAPI来计算和
int sum numbers.stream().mapToInt(Integer::intValue).sum();通过使用流API我们可以将计算和的操作转换为一条流水线从而提高了代码的性能。
需要注意的是优化并不总是必要的。在大多数情况下代码的可读性和可维护性更为重要。只有在性能问题确实存在并且通过优化可以获得明显的性能提升时才应该进行优化。否则过度优化可能会带来更多的问题和麻烦。
第68条遵守普遍接受的命名惯例
遵守普遍接受的命名惯例Adhere to generally accepted naming conventions。命名是编程中非常重要的一部分良好的命名可以使代码更易读、易理解和易维护。遵守普遍接受的命名惯例可以使代码更具一致性并与其他开发人员共享代码时更易于理解。
以下是一些普遍接受的命名惯例 使用有意义的名称变量、方法和类的名称应该能够清楚地表达其用途和含义。避免使用无意义的名称或缩写以免给其他人阅读代码带来困扰。 使用驼峰命名法驼峰命名法是一种常见的命名约定其中单词之间使用大写字母分隔。例如myVariableName、calculateSum等。 使用具体的名称尽量使用具体的名称来描述变量、方法和类的用途。例如使用firstName而不是name来表示一个人的名字。 避免使用缩写尽量避免使用缩写除非它们是广为接受的缩写。如果必须使用缩写应该在注释或文档中解释其含义。 使用一致的命名风格在整个代码库中使用一致的命名风格可以提高代码的可读性。例如如果使用驼峰命名法那么所有的变量、方法和类都应该遵循这个命名风格。
下面是一个示例演示了如何遵守普遍接受的命名惯例
public class NamingConventionExample {private int numberOfStudents;private String studentName;public void setNumberOfStudents(int numberOfStudents) {this.numberOfStudents numberOfStudents;}public String getStudentName() {return studentName;}public void setStudentName(String studentName) {this.studentName studentName;}
}在上面的示例中我们使用驼峰命名法来命名变量和方法。变量numberOfStudents和studentName具有具体的名称能够清楚地表达其用途。同时我们还遵循了一致的命名风格使代码更易读和易理解。
遵守普遍接受的命名惯例可以使代码更易于理解和维护并与其他开发人员共享代码时更具可读性。因此在编写代码时应该始终遵守这些命名惯例。
第十章 异常
充分发挥异常的优点可以提高程序的可读性可靠性和可维护性。如果使用不当它们也会带来负面的影响。本章提供了一些关于有效使用异常的指导原则。
第69条只针对异常的情况才使用异常
在Java中异常处理机制是一种用于处理程序运行时错误和异常情况的机制。然而异常处理机制的开销相对较高因此不应该滥用异常。只有在以下情况下才应该使用异常处理 异常是正常的控制流之外的情况异常应该用于处理那些在正常情况下不应该发生的错误或异常情况。例如当尝试打开一个不存在的文件时会抛出FileNotFoundException异常这是一种正常控制流之外的情况。 异常是无法通过返回值进行处理的情况有些错误或异常情况无法通过返回特定的值来处理这时可以使用异常处理机制。例如当尝试除以零时会抛出ArithmeticException异常这是一种无法通过返回值来处理的情况。 异常是需要中断当前执行流程的情况有些错误或异常情况需要中断当前的执行流程并进行相应的处理。异常处理机制提供了一种方便的方式来中断当前的执行流程并跳转到异常处理代码块。例如当发生网络连接错误时可以抛出IOException异常中断当前的网络操作并进行相应的错误处理。
然而如果异常处理被滥用会导致代码的可读性和性能下降。因此应该遵循以下准则来正确使用异常处理机制 不要将异常用于正常的控制流程异常处理应该用于处理异常情况而不是作为正常的控制流程的一部分。如果某个操作的结果是可以预见的并且可以通过返回值进行处理那么就不应该使用异常。 不要将异常用于性能优化异常处理机制的开销相对较高因此不应该将其用于性能优化。如果某个操作的错误或异常情况是可以预见的并且可以通过返回特定的值来处理那么就应该使用返回值而不是异常。 使用标准的异常类Java提供了一系列标准的异常类用于表示常见的错误和异常情况。应该优先使用这些标准的异常类而不是自定义异常类。 提供有意义的异常信息在抛出异常时应该提供有意义的异常信息以便于调试和错误处理。异常信息应该清晰地描述异常的原因和上下文信息。
以下是一个示例演示了如何正确使用异常处理机制
public class FileProcessor {public void processFile(String filePath) throws FileNotFoundException, IOException {try (FileInputStream fis new FileInputStream(filePath)) {// 读取文件内容并进行处理} catch (FileNotFoundException e) {// 文件不存在的异常处理System.err.println(File not found: filePath);throw e;} catch (IOException e) {// 文件读取错误的异常处理System.err.println(Error reading file: filePath);throw e;}}
}在上述示例中processFile方法用于处理文件内容。如果文件不存在或者读取文件时发生错误会抛出相应的异常并在异常处理代码块中进行错误处理。这里使用了标准的异常类FileNotFoundException和IOException并提供了有意义的异常信息。这样调用者可以根据异常信息进行相应的错误处理。 对于一个返回类型为void的方法如果在service层发生了错误可以考虑以下几种方式来处理错误
返回布尔值可以将方法的返回类型改为boolean在方法执行成功时返回true在发生错误时返回false。调用方可以根据返回值来判断方法是否执行成功并根据需要进行相应的处理。
public boolean addData(Data data) {try {// 处理数据return true;} catch (Exception e) {// 处理错误return false;}
}使用回调函数可以将错误处理逻辑封装成一个回调函数并将其作为参数传递给方法。在方法执行过程中如果发生错误可以调用回调函数来处理错误。
public void addData(Data data, ErrorCallback errorCallback) {try {// 处理数据} catch (Exception e) {// 处理错误errorCallback.onError(e);}
}使用返回值对象可以定义一个包含执行结果和错误信息的返回值对象将其作为方法的返回值。在方法执行成功时返回包含执行结果的对象在发生错误时返回包含错误信息的对象。
public Result addData(Data data) {try {// 处理数据return new Result(true, 添加成功);} catch (Exception e) {// 处理错误return new Result(false, 添加失败 e.getMessage());}
}public class Result {private boolean success;private String message;// getters and setters
}以上是几种处理错误的方式您可以根据具体的业务需求和开发团队的约定选择适合的方式。无论选择哪种方式都应该保证错误信息能够传递给调用方并且能够进行相应的错误处理。
第70条对可恢复的情况使用受检异常对编程错误使用运行时异常
受检异常Checked Exception是指在方法声明中显式声明的异常调用该方法时必须处理或者继续抛出该异常。受检异常通常表示程序在运行过程中可能遇到的外部条件或错误需要在编译时进行处理以保证程序的健壮性和可靠性。
运行时异常Runtime Exception是指在方法声明中没有显式声明的异常调用该方法时可以选择处理或者继续抛出该异常。运行时异常通常表示程序中的编程错误或逻辑错误是由程序员在编码过程中犯下的错误需要在运行时进行调试和修复。
我们应该将可能发生的可恢复的情况抛出受检异常以强制调用方在编译时处理这些异常。这样可以提醒调用方注意可能发生的异常情况并且可以在编译时捕获和处理这些异常以保证程序的正确性和可靠性。
而对于编程错误或逻辑错误我们应该抛出运行时异常。这样可以在运行时快速发现并修复这些错误同时也可以减少代码中的冗余异常处理逻辑提高代码的可读性和可维护性。
下面是一个示例演示了如何根据第70条的建议来使用受检异常和运行时异常
// 受检异常示例
public class FileProcessor {public void processFile(String filePath) throws FileNotFoundException, IOException {try {// 打开文件FileInputStream fileInputStream new FileInputStream(filePath);// 处理文件// ...// 关闭文件fileInputStream.close();} catch (FileNotFoundException e) {// 处理文件不存在的情况throw e;} catch (IOException e) {// 处理文件读写错误的情况throw e;}}
}// 运行时异常示例
public class Calculator {public int divide(int dividend, int divisor) {if (divisor 0) {throw new ArithmeticException(除数不能为0);}return dividend / divisor;}
}在上面的示例中FileProcessor类中的processFile方法抛出了受检异常FileNotFoundException和IOException调用方必须在编译时处理这些异常以保证文件的正确处理。
而Calculator类中的divide方法抛出了运行时异常ArithmeticException这是一个表示算术错误的异常它是由程序员在编码过程中犯下的错误。调用方可以选择处理或者继续抛出这个异常以便在运行时进行调试和修复。
通过合理地使用受检异常和运行时异常我们可以提高程序的可靠性和可维护性同时也可以更好地区分可恢复的情况和编程错误。
第71条避免不必要地使用受检异常
在设计和使用异常时应该避免过度使用受检异常以免给程序的编写和使用带来不必要的复杂性和负担。
受检异常Checked Exception是指在方法声明中显式声明的异常调用该方法时必须处理或者继续抛出该异常。受检异常通常表示程序在运行过程中可能遇到的外部条件或错误需要在编译时进行处理以保证程序的健壮性和可靠性。
然而过度使用受检异常可能会导致以下问题 异常处理代码的冗余和复杂性每次调用一个可能抛出受检异常的方法时都需要编写相应的异常处理代码这会增加代码的冗余和复杂性。 异常处理代码的传递性如果一个方法抛出了受检异常那么调用该方法的方法也必须处理或者继续抛出该异常这种异常处理代码的传递性可能会导致异常处理代码的层层嵌套使得代码难以理解和维护。 异常处理代码的限制性受检异常的处理方式是固定的要么处理异常要么继续抛出异常。这种限制性可能会限制程序的灵活性和可扩展性。
因此根据第71条的建议我们应该避免不必要地使用受检异常只在真正需要时才使用受检异常。对于那些不太可能发生或者不太需要处理的异常情况可以考虑使用运行时异常或者其他方式来表示和处理。
下面是一个示例演示了如何避免不必要地使用受检异常
// 不必要地使用受检异常的示例
public class FileProcessor {public void processFile(String filePath) throws FileNotFoundException, IOException {try {// 打开文件FileInputStream fileInputStream new FileInputStream(filePath);// 处理文件// ...// 关闭文件fileInputStream.close();} catch (FileNotFoundException e) {// 处理文件不存在的情况throw e;} catch (IOException e) {// 处理文件读写错误的情况throw e;}}
}// 避免不必要地使用受检异常的示例
public class FileProcessor {public void processFile(String filePath) {try {// 打开文件FileInputStream fileInputStream new FileInputStream(filePath);// 处理文件// ...// 关闭文件fileInputStream.close();} catch (IOException e) {// 处理文件读写错误的情况throw new RuntimeException(文件处理错误, e);}}
}在上面的示例中第一个FileProcessor类中的processFile方法抛出了受检异常FileNotFoundException和IOException调用方必须在编译时处理这些异常。
而第二个FileProcessor类中的processFile方法没有显式声明任何受检异常而是将可能发生的异常包装成了运行时异常RuntimeException并抛出。这样可以避免调用方在编译时处理这些异常同时也减少了异常处理代码的冗余和复杂性。
通过避免不必要地使用受检异常我们可以简化代码提高代码的可读性和可维护性同时也可以减少异常处理代码的负担。
第72条优先使用标准的异常
在设计和使用异常时应该优先使用标准的异常类来表示常见的错误和异常情况而不是自定义异常类。
标准的异常类是指Java语言提供的已经定义好的异常类例如NullPointerException、IllegalArgumentException、IOException等。这些异常类具有更好的可读性和可维护性并且符合开发人员的预期。
使用标准的异常类有以下好处 可读性和可维护性标准的异常类具有明确的命名和语义可以更清晰地表达代码中的错误和异常情况提高代码的可读性和可维护性。 代码一致性使用标准的异常类可以使代码保持一致性使得不同的代码模块之间更容易理解和交流。 开发人员的预期标准的异常类是开发人员熟悉的他们已经习惯了处理这些异常类因此使用标准的异常类可以符合开发人员的预期减少错误和异常处理的困惑和错误。
下面是一个示例演示了如何优先使用标准的异常类
// 不优先使用标准的异常类的示例
public class CustomException extends Exception {// 自定义异常类// ...
}public class Calculator {public int divide(int dividend, int divisor) throws CustomException {if (divisor 0) {throw new CustomException(除数不能为0);}return dividend / divisor;}
}// 优先使用标准的异常类的示例
public class Calculator {public int divide(int dividend, int divisor) {if (divisor 0) {throw new IllegalArgumentException(除数不能为0);}return dividend / divisor;}
}在上面的示例中第一个Calculator类中的divide方法抛出了自定义的异常类CustomException这增加了代码的复杂性和可读性。
而第二个Calculator类中的divide方法使用了标准的异常类IllegalArgumentException来表示除数为0的错误情况。这样可以使代码更加简洁和易读同时也符合开发人员的预期。
通过优先使用标准的异常类我们可以提高代码的可读性和可维护性使得代码更加清晰和易于理解。同时也可以减少自定义异常类带来的复杂性和不必要的开销。
第73条抛出和抽象对应的异常
在设计和使用异常时应该抛出和抽象对应的异常以便于调用者能够更好地理解和处理异常情况。
抛出和抽象对应的异常意味着在方法声明中抛出的异常应该是方法实现中可能抛出的具体异常的抽象。这样做的好处是可以提供更高层次的异常信息使得调用者能够更好地理解和处理异常情况。
下面是一个示例演示了如何抛出和抽象对应的异常
// 不抛出和抽象对应的异常的示例
public class FileReader {public String readFile(String filePath) {try {// 读取文件的代码// ...} catch (IOException e) {// 处理异常的代码// ...}return null;}
}// 抛出和抽象对应的异常的示例
public class FileReader {public String readFile(String filePath) throws FileNotFoundException {try {// 读取文件的代码// ...} catch (IOException e) {throw new FileNotFoundException(文件不存在);}return null;}
}在上面的示例中第一个FileReader类中的readFile方法捕获了IOException异常并在异常处理代码中进行了处理。然而这样的处理方式并没有提供足够的异常信息给调用者调用者可能无法准确地知道发生了什么错误。
而第二个FileReader类中的readFile方法抛出了更具体的异常类FileNotFoundException并在异常处理代码中将IOException异常转换为FileNotFoundException异常。这样可以提供更高层次的异常信息使得调用者能够更好地理解和处理异常情况。
通过抛出和抽象对应的异常我们可以提供更准确和有意义的异常信息使得调用者能够更好地理解和处理异常情况。这样可以提高代码的可读性和可维护性同时也方便调试和排查问题。
第74条每个方法抛出的所有异常都要建立文档
在设计和使用异常时应该为每个方法明确地文档化该方法可能抛出的所有异常以便调用者能够了解和处理这些异常情况。
为每个方法建立文档可以提供以下好处 提供使用指导文档化异常可以告诉调用者该方法可能抛出哪些异常以及在什么情况下会抛出这些异常。这样可以帮助调用者正确地使用该方法并在必要时进行异常处理。 提高代码可读性文档化异常可以使代码更加清晰和易读。调用者可以通过查看方法的文档来了解该方法可能抛出的异常而不需要深入查看方法的实现细节。 方便异常处理文档化异常可以帮助调用者更好地处理异常情况。调用者可以根据文档中提供的信息选择适当的异常处理策略例如捕获异常并进行处理、向上层方法传递异常等。
下面是一个示例演示了如何使用throws为方法建立文档化异常
/*** 从数据库中获取用户信息* param userId 用户ID* return 用户信息* throws SQLException 如果数据库访问出现问题* throws UserNotFoundException 如果用户不存在*/
public UserInfo getUserInfo(String userId) throws SQLException, UserNotFoundException {// 从数据库中查询用户信息的代码// ...
}在上面的示例中getUserInfo方法的文档明确地列出了可能抛出的两种异常SQLException和UserNotFoundException。调用者可以根据文档中提供的信息正确地处理这些异常情况。
通过为每个方法建立文档化异常我们可以提供更清晰和准确的异常信息使得调用者能够更好地了解和处理异常情况。这样可以提高代码的可读性和可维护性并减少因异常处理不当而导致的错误。
第75条在细节消息中包含失败-捕获信息
在编写异常的细节消息时应该包含失败-捕获信息以便于调试和定位问题。
细节消息是异常对象中的一部分用于提供关于异常原因和上下文的详细信息。包含失败-捕获信息可以提供以下好处 提供调试信息包含失败-捕获信息可以帮助开发人员更好地理解异常的发生原因和上下文。这对于调试和定位问题非常有帮助。 提供错误追踪包含失败-捕获信息可以提供异常发生的堆栈跟踪信息从而可以追踪异常的发生路径。这对于定位问题和分析异常的传播路径非常有帮助。
下面是一个示例演示了如何在细节消息中包含失败-捕获信息
public class DatabaseConnectionException extends Exception {private String connectionUrl;public DatabaseConnectionException(String message, String connectionUrl) {super(message);this.connectionUrl connectionUrl;}Overridepublic String getMessage() {return super.getMessage() (Connection URL: connectionUrl );}
}在上面的示例中自定义的DatabaseConnectionException异常类包含了一个connectionUrl字段用于存储数据库连接的URL。在重写getMessage方法时将细节消息中包含了连接URL的信息。这样在抛出该异常时调用者可以通过异常对象的getMessage方法获取到包含连接URL的详细信息。
通过在细节消息中包含失败-捕获信息我们可以提供更详细和有用的异常信息帮助开发人员更好地理解和处理异常情况。这样可以加快问题定位和修复的速度并提高代码的可维护性。
第76条努力使失败保持原子性
在设计和实现方法时应该尽量保证方法的操作是原子的即要么全部成功执行要么全部失败以避免出现部分成功和部分失败的情况。
努力使失败保持原子性可以提供以下好处 数据一致性保持操作的原子性可以确保数据在操作过程中保持一致性。如果操作部分成功部分失败可能会导致数据不一致的情况发生。 简化错误处理保持操作的原子性可以简化错误处理的逻辑。如果操作失败可以直接抛出异常或返回错误码而不需要进行回滚或清理操作。
下面是一个示例演示了如何努力使失败保持原子性
public class BankAccount {private int balance;public synchronized void deposit(int amount) {balance amount;}public synchronized void withdraw(int amount) throws InsufficientFundsException {if (amount balance) {throw new InsufficientFundsException(Insufficient funds);}balance - amount;}
}在上面的示例中BankAccount类表示一个银行账户其中的deposit和withdraw方法都使用synchronized关键字进行同步以保证操作的原子性。如果在执行withdraw方法时发现余额不足会抛出自定义的InsufficientFundsException异常。
通过努力使失败保持原子性我们可以确保在多线程环境下对共享资源的操作是安全的并且可以避免数据不一致的情况发生。这样可以提高系统的可靠性和稳定性。
第77条不要忽略异常
在编写代码时应该避免忽略异常即不要仅仅使用空的catch块来捕获异常而不做任何处理。
不要忽略异常可以提供以下好处 提供错误处理异常是程序中可能出现的错误情况的表示。忽略异常意味着没有对错误进行处理可能导致程序继续执行下去产生更严重的问题。 提供调试信息异常通常包含有关错误原因和上下文的信息。忽略异常会导致这些信息丢失使得调试问题变得更加困难。
下面是一个示例演示了不要忽略异常的情况
public class FileProcessor {public void processFile(String filePath) {try {// 读取文件内容FileReader fileReader new FileReader(filePath);BufferedReader bufferedReader new BufferedReader(fileReader);String line;while ((line bufferedReader.readLine()) ! null) {// 处理文件内容System.out.println(line);}bufferedReader.close();} catch (IOException e) {// 空的catch块忽略异常}}
}在上面的示例中processFile方法用于处理文件内容。在读取文件内容的过程中使用了FileReader和BufferedReader来读取文件并对每一行进行处理。然而在异常处理中使用了一个空的catch块来忽略IOException异常。
这种情况下如果在读取文件时发生了IO错误程序将继续执行下去而不会对错误进行处理。这可能导致文件内容无法正确处理或者产生其他不可预料的问题。
为了避免忽略异常我们应该在catch块中添加适当的处理逻辑例如记录日志、抛出新的异常或者进行回滚操作以确保错误得到适当的处理。
第十一章 并发
线程机制允许同时进行多个活动。并发程序设计比单线程程序设计要困难得多因为有更多得东西可能出错也很难重现失败。但是你无法避免并发因为我们所做的大部分事情都需要并发并且并发也是能否从多核的处理器中获得好的性能的一个条件这些现在都是很平常的事了。本章阐述的建议可以帮助你编写出清晰、正确、文档组织良好的并发程序。
第78条同步访问共享的可变数据
在多线程环境下当多个线程同时访问和修改共享的可变数据时应该使用同步机制来保证数据的一致性和线程安全性。
同步访问共享的可变数据可以提供以下好处 数据一致性同步机制可以确保多个线程对共享数据的访问和修改是有序的避免出现数据不一致的情况。 线程安全性同步机制可以保证多个线程对共享数据的访问是互斥的避免出现竞态条件和并发问题。
下面是一个示例演示了同步访问共享的可变数据的情况
public class Counter {private int count;public synchronized void increment() {count;}public synchronized void decrement() {count--;}public synchronized int getCount() {return count;}
}在上面的示例中Counter类表示一个计数器其中的count变量是共享的可变数据。为了保证多个线程对count的访问和修改是同步的我们使用了synchronized关键字来修饰increment、decrement和getCount方法。
通过使用synchronized关键字我们确保了每次对count的访问和修改都是原子的避免了多个线程同时修改count导致的数据不一致性和线程安全性问题。
需要注意的是同步机制会引入一定的性能开销因此在设计和实现时需要权衡性能和线程安全性的需求。在某些情况下可以使用更细粒度的同步机制如使用锁或并发容器来提高并发性能。
第79条避免过度同步
在设计和实现多线程程序时应该避免过度使用同步机制只在必要的地方使用同步以避免性能下降和死锁等问题。
避免过度同步可以提供以下好处 提高性能同步机制会引入一定的性能开销包括线程切换、锁竞争等。过度使用同步会导致性能下降降低程序的并发性能。 避免死锁过度使用同步可能导致死锁的发生。当多个线程相互等待对方释放锁时就会发生死锁导致程序无法继续执行。
下面是一个示例演示了避免过度同步的情况
public class Counter {private int count;public void increment() {synchronized (this) {count;}}public int getCount() {synchronized (this) {return count;}}
}在上面的示例中Counter类表示一个计数器其中的count变量是共享的可变数据。为了保证多个线程对count的访问和修改是同步的我们使用了synchronized关键字来修饰increment和getCount方法。
然而我们只在必要的地方使用了同步机制即在对count进行访问和修改的代码块中。这样可以避免过度同步提高了程序的并发性能。
需要注意的是在设计和实现时需要仔细考虑同步的粒度和范围以确保线程安全性的同时尽量减少同步的开销。可以使用锁分离、细粒度同步等技术来优化同步机制。
第80条executor、task和stream优先于线程
在编写多线程程序时应该优先使用Executor框架、Task和Stream API来管理和执行任务而不是直接使用线程。
使用Executor、Task和Stream可以提供以下好处 简化编程模型使用Executor框架可以将任务的提交和执行进行解耦使得编程模型更加简单和易于理解。通过将任务封装成Runnable或Callable对象并提交给Executor来执行可以避免手动创建和管理线程的复杂性。 提高可维护性使用Executor框架可以更好地组织和管理任务使得代码结构更清晰和可维护。通过使用ExecutorService接口提供的方法可以方便地控制任务的执行、取消和获取执行结果等操作。 提高性能Executor框架可以根据实际情况自动管理线程池根据系统资源和任务负载的情况动态调整线程数量从而提高程序的性能和效率。
下面是一个示例演示了使用Executor框架来执行任务的情况
public class Task implements Runnable {private int taskId;public Task(int taskId) {this.taskId taskId;}Overridepublic void run() {System.out.println(Task taskId is running.);}
}public class Main {public static void main(String[] args) {ExecutorService executor Executors.newFixedThreadPool(5);for (int i 0; i 10; i) {Task task new Task(i);executor.execute(task);}executor.shutdown();}
}在上面的示例中我们定义了一个Task类实现了Runnable接口表示一个任务。然后我们使用ExecutorService接口提供的execute方法将任务提交给线程池执行。
通过使用Executor框架我们可以方便地管理和执行任务而不需要手动创建和管理线程。同时线程池可以根据需要动态调整线程数量提高程序的性能和效率。
需要注意的是Executor框架还提供了其他的功能和特性如定时执行任务、获取任务执行结果等可以根据实际需求进行使用。此外Java 8引入的Stream API也提供了一种更加简洁和函数式的方式来处理集合数据的并行操作可以进一步简化多线程编程。
第81条并发工具优先于wait和notify
在编写多线程程序时应该优先使用Java并发工具类如Lock、Condition、Semaphore等来实现线程间的协作和同步而不是直接使用wait和notify方法。
使用并发工具类可以提供以下好处 更安全的线程同步并发工具类提供了更高级别的线程同步机制可以更安全地实现线程间的协作和同步。相比于wait和notify方法它们提供了更细粒度的控制和更强大的功能可以避免一些常见的线程同步问题如死锁、饥饿等。 更灵活的线程协作并发工具类提供了更灵活的线程协作方式可以实现更复杂的线程间通信和同步逻辑。例如使用Condition接口可以实现更细粒度的等待和唤醒机制可以根据特定条件来控制线程的执行。 更好的性能和可伸缩性并发工具类在设计上考虑了性能和可伸缩性可以更好地利用多核处理器和线程池等资源提高程序的性能和并发能力。
下面是一个示例演示了使用Lock和Condition来实现线程间的协作和同步的情况
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class Task {private Lock lock new ReentrantLock();private Condition condition lock.newCondition();private boolean isReady false;public void doSomething() throws InterruptedException {lock.lock();try {while (!isReady) {condition.await();}// 执行任务逻辑System.out.println(Task is running.);} finally {lock.unlock();}}public void setReady() {lock.lock();try {isReady true;condition.signalAll();} finally {lock.unlock();}}
}public class Main {public static void main(String[] args) throws InterruptedException {Task task new Task();Thread thread1 new Thread(() - {try {task.doSomething();} catch (InterruptedException e) {e.printStackTrace();}});Thread thread2 new Thread(() - {task.setReady();});thread1.start();Thread.sleep(1000); // 等待1秒钟thread2.start();}
}在上面的示例中我们定义了一个Task类其中包含一个Lock对象和一个Condition对象用于实现线程间的协作和同步。在doSomething方法中线程会等待isReady变量为true然后执行任务逻辑。在setReady方法中线程会将isReady变量设置为true并通过signalAll方法唤醒等待的线程。
通过使用Lock和Condition我们可以更安全地实现线程间的协作和同步避免了直接使用wait和notify方法可能引发的问题。同时Lock和Condition提供了更灵活的线程协作方式可以根据实际需求进行控制和调整。
第82条线程安全性的文档化
在编写多线程程序时应该明确地文档化每个类或方法的线程安全性以便其他开发人员能够正确地使用和理解这些类或方法。
线程安全性的文档化可以提供以下好处 提供使用指南通过明确地文档化线程安全性可以为其他开发人员提供使用指南告知他们如何正确地使用和调用这些类或方法。这可以避免一些常见的线程安全问题如竞态条件、数据不一致等。 增强可维护性文档化线程安全性可以增强代码的可维护性。当其他开发人员需要修改或扩展已有的线程安全类或方法时他们可以根据文档了解到哪些部分是线程安全的哪些部分需要额外的同步措施。 促进代码审查和测试文档化线程安全性可以促进代码审查和测试的进行。其他开发人员可以根据文档来检查代码是否符合线程安全的要求并进行相应的测试和验证。
下面是一个示例演示了如何文档化线程安全性
/*** 线程安全的计数器类*/
public class Counter {private int count;/*** 增加计数器的值* 线程安全多个线程可以同时调用该方法而不会出现竞态条件*/public synchronized void increment() {count;}/*** 获取计数器的值* 线程安全多个线程可以同时调用该方法而不会出现竞态条件*/public synchronized int getCount() {return count;}
}在上面的示例中我们定义了一个线程安全的计数器类Counter。通过使用synchronized关键字修饰increment和getCount方法我们确保了多个线程可以同时调用这些方法而不会出现竞态条件。
同时我们在类和方法的注释中明确地说明了这些方法的线程安全性告知其他开发人员可以安全地使用这些方法。
通过这样的文档化其他开发人员可以根据文档了解到Counter类是线程安全的并且可以在多线程环境下正确地使用和调用increment和getCount方法。
第83条慎用延迟初始化
在编写代码时应该慎重考虑是否使用延迟初始化因为延迟初始化可能会引入一些潜在的问题和复杂性。
延迟初始化是指在需要时才进行对象的初始化而不是在对象创建时立即进行初始化。延迟初始化的目的是为了延迟对象的创建和初始化过程以提高性能和节省资源。
然而延迟初始化可能会引入以下问题 线程安全性问题延迟初始化通常需要使用同步机制来保证线程安全性。如果不正确地处理线程安全性可能会导致竞态条件和数据不一致的问题。 复杂性增加延迟初始化会增加代码的复杂性因为需要处理对象的创建和初始化时机。这可能会导致代码更难理解、维护和调试。 性能损失延迟初始化可能会导致性能损失因为在第一次使用对象之前需要进行额外的初始化操作。如果对象的初始化成本较高延迟初始化可能会导致性能下降。
下面是一个示例演示了延迟初始化可能引入的问题
public class LazyInitializationExample {private ExpensiveObject expensiveObject;public ExpensiveObject getExpensiveObject() {if (expensiveObject null) {expensiveObject new ExpensiveObject();}return expensiveObject;}
}在上面的示例中我们定义了一个LazyInitializationExample类其中包含一个expensiveObject对象。在getExpensiveObject方法中我们使用延迟初始化的方式来创建和返回expensiveObject对象。
然而这种延迟初始化的方式存在线程安全性问题。如果多个线程同时调用getExpensiveObject方法并且expensiveObject为null那么它们可能会同时执行对象的创建和初始化操作导致竞态条件和数据不一致的问题。
为了解决这个问题我们可以使用双重检查锁定double-checked locking来确保线程安全性
public class LazyInitializationExample {private volatile ExpensiveObject expensiveObject;public ExpensiveObject getExpensiveObject() {if (expensiveObject null) {synchronized (this) {if (expensiveObject null) {expensiveObject new ExpensiveObject();}}}return expensiveObject;}
}在上面的示例中我们使用了双重检查锁定来保证线程安全性。通过使用volatile关键字修饰expensiveObject变量我们确保了多个线程在访问expensiveObject时能够看到最新的值。同时通过在同步块内再次检查expensiveObject是否为null我们避免了多个线程同时执行对象的创建和初始化操作。
需要注意的是双重检查锁定需要在Java 5及以上版本中使用并且需要将expensiveObject变量声明为volatile。此外双重检查锁定也可能存在一些细微的问题因此在使用时需要仔细考虑和测试。 volatile关键字在Java中用于确保变量的可见性和禁止指令重排序。 可见性当一个变量被声明为volatile时它的值在多个线程之间是可见的。也就是说当一个线程修改了volatile变量的值时其他线程能够立即看到最新的值而不会使用缓存中的旧值。这样可以避免由于线程之间的数据不一致性而引发的问题。 禁止指令重排序在Java中编译器和处理器可能会对指令进行重排序以提高程序的执行效率。然而有时候指令重排序可能会导致程序的行为出现问题。当一个变量被声明为volatile时编译器和处理器会禁止对该变量的指令重排序从而确保程序的执行顺序符合预期。
需要注意的是volatile关键字只能保证单个变量的可见性和禁止指令重排序并不能保证一系列操作的原子性。如果需要保证一系列操作的原子性可以考虑使用synchronized关键字或java.util.concurrent.atomic包中的原子类。
下面是一个示例演示了volatile关键字的作用
public class VolatileExample {private volatile boolean flag false;public void setFlag(boolean value) {flag value;}public void printFlag() {System.out.println(Flag: flag);}
}在上面的示例中我们定义了一个VolatileExample类其中包含一个flag变量。在setFlag方法中我们将flag的值设置为指定的值。在printFlag方法中我们打印flag的值。
如果flag变量没有被声明为volatile那么在一个线程中调用setFlag方法修改flag的值后另一个线程调用printFlag方法可能会看到旧的值因为没有保证可见性。但是如果将flag变量声明为volatile那么在一个线程中调用setFlag方法修改flag的值后另一个线程调用printFlag方法将能够立即看到最新的值保证了可见性。
需要注意的是volatile关键字的使用需要谨慎只有在确实需要保证可见性和禁止指令重排序的情况下才使用。过度使用volatile关键字可能会导致性能下降。
第84条不要依赖于线程调度器
在编写多线程程序时应该避免依赖于线程调度器的行为因为线程调度器的行为是不确定的可能会导致程序的行为出现问题。
线程调度器是操作系统的一部分负责决定哪个线程在某个时间点上运行。线程调度器根据一些策略如时间片轮转、优先级等来决定线程的执行顺序。然而线程调度器的行为是不可预测的不同的操作系统和硬件平台可能有不同的实现和策略。
依赖于线程调度器的行为可能会导致以下问题 竞态条件如果程序的正确性依赖于线程的执行顺序那么在不同的操作系统和硬件平台上可能会出现不同的线程执行顺序从而导致竞态条件和数据不一致性。 死锁如果程序中存在死锁的情况依赖于线程调度器的行为可能会导致死锁的发生或解决。
为了避免依赖于线程调度器的行为可以采取以下措施 使用同步机制使用同步机制如锁、信号量等来保证线程之间的协调和同步而不依赖于线程调度器的行为。 使用线程池使用线程池来管理线程的创建和执行线程池可以提供更可控的线程执行环境而不依赖于线程调度器的行为。
下面是一个示例演示了不要依赖于线程调度器的问题
public class ThreadSchedulerExample {private static boolean flag false;public static void main(String[] args) {Thread thread1 new Thread(() - {while (!flag) {// do something}System.out.println(Thread 1 finished);});Thread thread2 new Thread(() - {flag true;System.out.println(Thread 2 finished);});thread1.start();thread2.start();}
}在上面的示例中我们创建了两个线程thread1和thread2。thread1在一个循环中等待flag变量的值为true而thread2在将flag变量的值设置为true后输出一条消息。
如果我们依赖于线程调度器的行为那么我们期望thread2先执行将flag的值设置为true然后thread1检测到flag的值为true后退出循环。然而由于线程调度器的行为是不确定的实际上可能会出现thread1先执行的情况导致thread1陷入无限循环程序无法正常结束。
为了解决这个问题我们可以使用同步机制如volatile关键字或锁来保证flag变量的可见性和同步而不依赖于线程调度器的行为。
第十二章 序列化
本章讨论对象序列化它是java的一个框架用来将对象编码成字节流序列化并从字节流编码中重新构建对象反序列化。一旦对象被序列化它的编码就可以从一台正在运行的虚拟机被传递到另一台虚拟机上或者被存储到磁盘上供后续反序列化使用。本章主要关注序列化的风险以及如何将风险降到最低。
第85条其他方法优先于Java序列化
在设计可序列化的类时应该优先考虑其他方法而不是依赖于Java序列化机制。
Java序列化是一种将对象转换为字节流的机制可以用于对象的持久化、网络传输等场景。然而Java序列化机制存在一些问题和限制 性能问题Java序列化机制的性能通常较低序列化和反序列化过程需要大量的时间和资源。 版本兼容性问题当类的结构发生变化时如添加、删除或修改字段使用Java序列化机制的类可能会导致版本兼容性问题。反序列化时如果序列化的字节流与当前类的结构不匹配会抛出InvalidClassException。 安全问题Java序列化机制存在安全风险恶意的序列化数据可能导致远程代码执行、拒绝服务等安全问题。
为了避免Java序列化机制的问题可以考虑以下替代方法 自定义序列化通过实现Serializable接口的writeObject和readObject方法手动控制对象的序列化和反序列化过程。这样可以提高性能并且可以处理版本兼容性问题。 使用JSON或XML序列化将对象转换为JSON或XML格式的字符串可以使用第三方库如Jackson、Gson、XStream等来实现。这种方式通常比Java序列化更高效并且具有更好的版本兼容性。 使用协议缓冲区Protocol Buffers协议缓冲区是一种轻量级、高效的序列化机制可以将结构化数据序列化为二进制格式。它具有较高的性能和较小的序列化大小。
下面是一个示例演示了使用JSON序列化代替Java序列化的情况
import com.google.gson.Gson;public class SerializationExample {public static void main(String[] args) {Person person new Person(John, 25);// 使用Java序列化byte[] serializedData serialize(person);Person deserializedPerson deserialize(serializedData);System.out.println(deserializedPerson.getName()); // 输出John// 使用JSON序列化String json toJson(person);Person deserializedPerson2 fromJson(json);System.out.println(deserializedPerson2.getName()); // 输出John}// 使用Java序列化private static byte[] serialize(Person person) {// 实现序列化逻辑// ...return null;}private static Person deserialize(byte[] data) {// 实现反序列化逻辑// ...return null;}// 使用JSON序列化private static String toJson(Person person) {Gson gson new Gson();return gson.toJson(person);}private static Person fromJson(String json) {Gson gson new Gson();return gson.fromJson(json, Person.class);}
}class Person {private String name;private int age;// 省略构造方法、getter和setter// 使用Java序列化private void writeObject(java.io.ObjectOutputStream out) throws IOException {// 实现自定义序列化逻辑// ...}private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {// 实现自定义反序列化逻辑// ...}
}在上面的示例中我们定义了一个Person类包含name和age字段。我们使用Java序列化和JSON序列化分别对Person对象进行序列化和反序列化。
通过比较使用Java序列化和JSON序列化的方式我们可以看到JSON序列化更简洁、性能更高并且不会受到版本兼容性问题的影响。因此根据第85条的建议我们应该优先考虑使用其他方法而不是Java序列化。
第86条谨慎地实现Serializable接口
在实现Serializable接口时需要谨慎考虑类的可序列化性并采取适当的措施来保护类的不变性和安全性。
Serializable接口是Java提供的一个标记接口用于标识一个类可以被序列化。当一个类实现了Serializable接口它的对象可以被转换为字节流以便在网络传输、持久化等场景中使用。然而实现Serializable接口可能会引入一些问题 版本兼容性问题当类的结构发生变化时如添加、删除或修改字段使用Java序列化机制的类可能会导致版本兼容性问题。反序列化时如果序列化的字节流与当前类的结构不匹配会抛出InvalidClassException。 安全问题Java序列化机制存在安全风险恶意的序列化数据可能导致远程代码执行、拒绝服务等安全问题。
为了谨慎实现Serializable接口可以采取以下措施 显式声明serialVersionUIDserialVersionUID是一个序列化版本号用于标识类的版本。在类的结构发生变化时可以通过显式声明serialVersionUID来控制版本兼容性。如果不显式声明serialVersionUIDJava序列化机制会根据类的结构自动生成一个版本号这可能导致版本兼容性问题。 谨慎处理不可序列化的字段如果一个类中包含不可序列化的字段可以通过自定义序列化和反序列化方法来处理这些字段。在writeObject方法中可以手动将不可序列化的字段转换为可序列化的形式在readObject方法中可以手动将可序列化的字段转换为不可序列化的形式。 谨慎处理敏感信息如果一个类中包含敏感信息如密码、密钥等应该考虑将这些字段标记为transient以防止被序列化。在writeObject方法中可以清除敏感信息在readObject方法中可以重新初始化敏感信息。
下面是一个示例演示了谨慎实现Serializable接口的情况
import java.io.*;public class SerializationExample {public static void main(String[] args) {Person person new Person(John, 25, password123);// 序列化byte[] serializedData serialize(person);// 反序列化Person deserializedPerson deserialize(serializedData);System.out.println(deserializedPerson.getName()); // 输出JohnSystem.out.println(deserializedPerson.getAge()); // 输出25System.out.println(deserializedPerson.getPassword()); // 输出null}private static byte[] serialize(Person person) {try (ByteArrayOutputStream baos new ByteArrayOutputStream();ObjectOutputStream oos new ObjectOutputStream(baos)) {oos.writeObject(person);return baos.toByteArray();} catch (IOException e) {e.printStackTrace();return null;}}private static Person deserialize(byte[] data) {try (ByteArrayInputStream bais new ByteArrayInputStream(data);ObjectInputStream ois new ObjectInputStream(bais)) {return (Person) ois.readObject();} catch (IOException | ClassNotFoundException e) {e.printStackTrace();return null;}}
}class Person implements Serializable {private static final long serialVersionUID 1L;private String name;private int age;private transient String password;public Person(String name, int age, String password) {this.name name;this.age age;this.password password;}public String getName() {return name;}public int getAge() {return age;}public String getPassword() {return password;}private void writeObject(ObjectOutputStream out) throws IOException {out.defaultWriteObject();out.writeObject(encryptPassword(password));}private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {in.defaultReadObject();password decryptPassword((String) in.readObject());}private String encryptPassword(String password) {// 实现密码加密逻辑// ...return null;}private String decryptPassword(String encryptedPassword) {// 实现密码解密逻辑// ...return null;}
}在上面的示例中我们定义了一个Person类实现了Serializable接口。Person类包含name、age和password字段其中password字段被标记为transient以防止被序列化。
在Person类中我们显式声明了serialVersionUID并实现了writeObject和readObject方法来处理不可序列化的字段password。在writeObject方法中我们将password字段加密后序列化在readObject方法中我们将序列化的password字段解密后重新初始化。
通过以上措施我们可以谨慎地实现Serializable接口保护类的不变性和安全性并处理版本兼容性问题。根据第86条的建议我们应该在实现Serializable接口时谨慎考虑类的可序列化性并采取适当的措施来保护类的不变性和安全性。
第87条考虑使用自定义的序列化形式
在实现Serializable接口时可以考虑使用自定义的序列化形式以提高性能、灵活性和安全性。
Java的序列化机制会自动将对象的所有字段进行序列化和反序列化包括私有字段和继承的字段。然而有时候我们可能只需要序列化对象的一部分字段或者需要对字段进行特殊处理。这时可以使用自定义的序列化形式来满足需求。
使用自定义的序列化形式可以带来以下好处 提高性能自定义的序列化形式可以选择性地序列化对象的字段避免不必要的序列化操作从而提高性能。 灵活性自定义的序列化形式可以处理不可序列化的字段如transient字段、静态字段等。通过自定义的序列化方法可以手动处理这些字段的序列化和反序列化。 安全性自定义的序列化形式可以对敏感信息进行加密、解密等处理提高数据的安全性。
下面是一个示例演示了使用自定义的序列化形式的情况
import java.io.*;public class CustomSerializationExample {public static void main(String[] args) {Person person new Person(John, 25, password123);// 序列化byte[] serializedData serialize(person);// 反序列化Person deserializedPerson deserialize(serializedData);System.out.println(deserializedPerson.getName()); // 输出JohnSystem.out.println(deserializedPerson.getAge()); // 输出25System.out.println(deserializedPerson.getPassword()); // 输出null}private static byte[] serialize(Person person) {try (ByteArrayOutputStream baos new ByteArrayOutputStream();ObjectOutputStream oos new ObjectOutputStream(baos)) {oos.writeObject(person);return baos.toByteArray();} catch (IOException e) {e.printStackTrace();return null;}}private static Person deserialize(byte[] data) {try (ByteArrayInputStream bais new ByteArrayInputStream(data);ObjectInputStream ois new ObjectInputStream(bais)) {return (Person) ois.readObject();} catch (IOException | ClassNotFoundException e) {e.printStackTrace();return null;}}
}class Person implements Serializable {private static final long serialVersionUID 1L;private String name;private int age;private transient String password;public Person(String name, int age, String password) {this.name name;this.age age;this.password password;}private void writeObject(ObjectOutputStream out) throws IOException {out.defaultWriteObject();out.writeObject(encryptPassword(password));}private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {in.defaultReadObject();password decryptPassword((String) in.readObject());}private String encryptPassword(String password) {// 实现密码加密逻辑// ...return null;}private String decryptPassword(String encryptedPassword) {// 实现密码解密逻辑// ...return null;}
}在上面的示例中我们定义了一个Person类实现了Serializable接口。Person类包含name、age和password字段其中password字段被标记为transient以防止被序列化。
在Person类中我们实现了writeObject和readObject方法来处理自定义的序列化形式。在writeObject方法中我们将password字段加密后序列化在readObject方法中我们将序列化的password字段解密后重新初始化。
通过自定义的序列化形式我们可以灵活地处理字段的序列化和反序列化提高性能和安全性。根据第87条的建议我们应该考虑使用自定义的序列化形式以满足特定的需求。
第88条保护性地编写readObject方法
在实现自定义的readObject方法时需要采取一些措施来保护类的不变性和安全性。
在Java的序列化机制中readObject方法用于反序列化对象。当一个类实现了Serializable接口并定义了readObject方法时该方法会在反序列化过程中被调用用于恢复对象的状态。
然而由于readObject方法可以访问对象的私有字段和方法它可能会被恶意使用来破坏类的不变性和安全性。为了防止这种情况发生我们需要保护性地编写readObject方法采取以下措施 检查输入参数在readObject方法中应该对输入参数进行检查确保它们符合预期的格式和内容。如果输入参数不符合要求可以抛出InvalidObjectException来阻止对象的反序列化。 使用readResolve方法如果一个类实现了readObject方法那么最好也实现readResolve方法。readResolve方法可以在对象反序列化后被调用用于返回一个替代的对象。通过使用readResolve方法可以确保反序列化后的对象是预期的对象而不是readObject方法中创建的新对象。 使用ObjectInputValidation接口ObjectInputValidation接口可以用于在反序列化过程中对对象进行验证。通过实现ObjectInputValidation接口并在readObject方法中注册验证对象可以在反序列化完成后对对象进行额外的验证操作。
下面是一个示例演示了保护性地编写readObject方法的情况
import java.io.*;public class ProtectiveReadObjectExample {public static void main(String[] args) {Person person new Person(John, 25);// 序列化byte[] serializedData serialize(person);// 反序列化Person deserializedPerson deserialize(serializedData);System.out.println(deserializedPerson.getName()); // 输出JohnSystem.out.println(deserializedPerson.getAge()); // 输出25}private static byte[] serialize(Person person) {try (ByteArrayOutputStream baos new ByteArrayOutputStream();ObjectOutputStream oos new ObjectOutputStream(baos)) {oos.writeObject(person);return baos.toByteArray();} catch (IOException e) {e.printStackTrace();return null;}}private static Person deserialize(byte[] data) {try (ByteArrayInputStream bais new ByteArrayInputStream(data);ObjectInputStream ois new ObjectInputStream(bais)) {return (Person) ois.readObject();} catch (IOException | ClassNotFoundException e) {e.printStackTrace();return null;}}
}class Person implements Serializable {private static final long serialVersionUID 1L;private String name;private int age;public Person(String name, int age) {this.name name;this.age age;}private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {in.defaultReadObject();// 检查输入参数if (age 0) {throw new InvalidObjectException(Invalid age);}}private Object readResolve() {// 返回预期的对象return new Person(name, age);}private void validateObject() throws InvalidObjectException {// 对象验证逻辑if (name null || name.isEmpty()) {throw new InvalidObjectException(Invalid name);}}
}在上面的示例中我们定义了一个Person类实现了Serializable接口。Person类包含name和age字段。
在Person类中我们实现了readObject方法来保护性地处理反序列化过程。在readObject方法中我们检查了age字段的值如果小于0则抛出InvalidObjectException。
此外我们还实现了readResolve方法和validateObject方法。readResolve方法返回一个预期的对象以确保反序列化后的对象是预期的对象。validateObject方法用于在反序列化完成后对对象进行验证如果验证失败则抛出InvalidObjectException。
通过保护性地编写readObject方法我们可以确保对象的不变性和安全性在反序列化过程中得到保护。根据第88条的建议我们应该在实现readObject方法时采取适当的措施以防止恶意使用和数据损坏。
第89条对于实例控制枚举类型优先于readResolve
在需要控制对象实例化的情况下枚举类型优先于readResolve方法。
在Java的序列化机制中readResolve方法可以用于在反序列化过程中返回一个替代的对象。通过实现readResolve方法可以确保反序列化后的对象是预期的对象而不是readObject方法中创建的新对象。
然而使用readResolve方法来控制对象实例化存在一些问题。首先readResolve方法只在反序列化时被调用而在其他情况下如反射、克隆等仍然可以创建新的对象。其次readResolve方法的实现可能会被继承类覆盖导致实例化控制失效。
相比之下枚举类型提供了更好的实例控制机制。枚举类型的实例是唯一的无法通过反射、克隆等方式创建新的实例。因此使用枚举类型可以确保对象的唯一性和实例控制。
下面是一个示例演示了使用枚举类型进行实例控制的情况
import java.io.*;public class InstanceControlExample {public static void main(String[] args) {Singleton singleton Singleton.INSTANCE;// 序列化byte[] serializedData serialize(singleton);// 反序列化Singleton deserializedSingleton deserialize(serializedData);System.out.println(singleton deserializedSingleton); // 输出true}private static byte[] serialize(Serializable object) {try (ByteArrayOutputStream baos new ByteArrayOutputStream();ObjectOutputStream oos new ObjectOutputStream(baos)) {oos.writeObject(object);return baos.toByteArray();} catch (IOException e) {e.printStackTrace();return null;}}private static T T deserialize(byte[] data) {try (ByteArrayInputStream bais new ByteArrayInputStream(data);ObjectInputStream ois new ObjectInputStream(bais)) {return (T) ois.readObject();} catch (IOException | ClassNotFoundException e) {e.printStackTrace();return null;}}
}enum Singleton implements Serializable {INSTANCE;private Singleton() {// 构造方法}
}在上面的示例中我们定义了一个枚举类型Singleton它实现了Serializable接口。Singleton枚举类型只有一个实例INSTANCE。
通过使用枚举类型我们可以确保Singleton类的实例是唯一的。在序列化和反序列化过程中INSTANCE实例会被正确地保留和恢复而不会创建新的实例。
相比之下如果我们使用readResolve方法来控制Singleton类的实例化可能会存在一些问题。例如如果readResolve方法被继承类覆盖那么实例化控制可能会失效。
因此根据第89条的建议当需要实例控制时枚举类型是一个更好的选择。枚举类型提供了更好的实例控制机制可以确保对象的唯一性和实例控制而不依赖于readResolve方法。
第90条考虑用序列化代理代替序列化实例
在某些情况下使用序列化代理可以提供更好的灵活性、安全性和性能。
在Java的序列化机制中对象的序列化和反序列化是通过对象的字段来完成的。当一个对象被序列化时它的所有字段都会被序列化。而当一个对象被反序列化时它的所有字段都会被恢复。
然而有时候直接序列化对象可能存在一些问题。例如如果对象的字段包含敏感信息直接序列化可能会导致敏感信息泄露。另外如果对象的字段发生变化直接序列化可能会导致反序列化失败。
为了解决这些问题可以使用序列化代理。序列化代理是一个中间类它充当了对象的代理负责对象的序列化和反序列化。通过使用序列化代理可以控制序列化和反序列化的过程从而提供更好的灵活性、安全性和性能。
下面是一个示例演示了使用序列化代理的情况
import java.io.*;public class SerializationProxyExample {public static void main(String[] args) {Person person new Person(John, 25);// 序列化byte[] serializedData serialize(person);// 反序列化Person deserializedPerson deserialize(serializedData);System.out.println(person.equals(deserializedPerson)); // 输出true}private static byte[] serialize(Serializable object) {try (ByteArrayOutputStream baos new ByteArrayOutputStream();ObjectOutputStream oos new ObjectOutputStream(baos)) {oos.writeObject(object);return baos.toByteArray();} catch (IOException e) {e.printStackTrace();return null;}}private static T T deserialize(byte[] data) {try (ByteArrayInputStream bais new ByteArrayInputStream(data);ObjectInputStream ois new ObjectInputStream(bais)) {return (T) ois.readObject();} catch (IOException | ClassNotFoundException e) {e.printStackTrace();return null;}}
}class Person implements Serializable {private String name;private int age;public Person(String name, int age) {this.name name;this.age age;}private Object writeReplace() {return new SerializationProxy(this);}private void readObject(ObjectInputStream stream) throws InvalidObjectException {throw new InvalidObjectException(Proxy required);}private static class SerializationProxy implements Serializable {private String name;private int age;public SerializationProxy(Person person) {this.name person.name;this.age person.age;}private Object readResolve() {return new Person(name, age);}}
}在上面的示例中我们定义了一个Person类它实现了Serializable接口。Person类有两个字段name和age。
为了使用序列化代理我们在Person类中定义了一个私有的writeReplace方法和一个私有的readObject方法。writeReplace方法返回一个序列化代理对象用于在序列化过程中替代Person对象。readObject方法抛出InvalidObjectException异常防止直接反序列化Person对象。
同时我们定义了一个私有的SerializationProxy类它实现了Serializable接口。SerializationProxy类包含了Person对象的字段并在readResolve方法中创建并返回Person对象。
通过使用序列化代理我们可以控制Person对象的序列化和反序列化过程。在序列化过程中Person对象会被替代为SerializationProxy对象从而保护了对象的字段。在反序列化过程中SerializationProxy对象会被替换为Person对象从而恢复了对象的状态。
因此根据第90条的建议当需要更好的灵活性、安全性和性能时可以考虑使用序列化代理代替直接序列化对象。通过使用序列化代理可以控制序列化和反序列化的过程从而提供更好的控制和保护。