关于如何写好代码的一些建议与方法(下)

文章目录

  • 三、设计原则
    • 3.1 单一职责
    • 3.2 开闭原则
    • 3.3 里式替换原则
    • 3.4 接口隔离原则
    • 3.5 依赖倒置原则
    • 3.6 迪米特法则
    • 3.7 DRY原则
    • 3.8 KISS原则
    • 总结
  • 四、设计模式
  • 五、日常踩坑
    • 5.1 避免不必要的对象创建
      • 5.1.1 不可变的对象
      • 5.1.2 静态方法
      • 5.1.3 视图
      • 5.1.4 自动装箱
    • 5.2 内存泄漏
      • 5.2.1 过期引用
      • 5.2.2 长生命周期引用短生命周期
      • 5.2.3 静态数据、缓存
    • 5.3 覆盖equals时总要覆盖hashCode
    • 5.4 clone
      • 数据类型
      • 赋值语句
      • 深拷贝和浅拷贝
        • 概念理解
        • 不可变对象
        • Cloneable接口
        • 含有引用类型的问题
          • 字节流
          • 重新实现
          • Hutool工具包
          • 总结
    • 5.5 Null值处理
      • 5.5.1 字符串
      • 5.5.2 Objects
      • 5.5.3 集合的处理
      • 5.5.4 避免传递Null值
      • 5.5.5 Optional
      • 5.5.6 JDK14改进
    • 5.6 遇到的一些坑
      • Number类的缓存
      • BigDecimal的精度
      • BigDecimal除不尽
      • BigDecimal比较
      • String.valueOf处理null值
      • Integer.parseInt转换
      • 集合的UnsupportedOperationException

关于如何写好代码的一些建议与方法(上)
关于如何写好代码的一些建议与方法(中)
关于如何写好代码的一些建议与方法(下)

三、设计原则

3.1 单一职责

一个类或者模块只负责完成一个职责,说明白点就是方法的功能要单一,上升到微服务就是业务领域的能力要单一。
单一职责概念上很简单,但要真正地去应用却不容易,如何判断一个职责是否单一本身就没有统一的标准,每个人处在不同的认知层次,那么对于单一职责的理解也就不一样,甚至有的时候,只要作用于不同业务场景下,单一职责的范围也会发生变化。

参考可能破坏单一职责的情况:

  • 一个类或者一个函数的代码行数过多。
  • 一个类或者一个函数依赖其他的地方过多。
  • 有多个动机,但修改却是同一个类。

JDBC大家都了解,一般通过JDBC连接并操作数据库都有几步标准的流程

1Connection负责与数据库的交互。
2Statement负责执行SQL、返回结果。
3ResultSet负责结果集处理。

上面的业务流程不经意间就可能被一气呵成地写完,我想大多数人平时在写业务代码时一定也是这样,想想JDBC这样设计的好处吧!

3.2 开闭原则

软件实体(模块、类、函数等等)应该是对扩展开放、对修改关闭的,大部分的设计模式都是为了解决代码扩展性的问题,遵从的就是开闭原则,所谓对扩展开放、对修改关闭指的就是当要添加一个新功能时,应该在已有代码的基础上添加新的类或者方法,而不是修改原来的类或者方法。

扩展开放:意味着当有需求变更时,可以对模块、类、函数等进行扩展,使其满足需求。
修改关闭:意味着当要对模块、类、函数等进行扩展时,不需要修改源代码;对于已经完成的类文件不需要重新编辑;对于已经编译打包好的模块,不需要再重新编译。

唯一不变的就是变化,我们应该在设计时让代码尽量能够适应变化,因此要求我们要有良好的抽象意识、封装意识,真正地面向对象开发,而不是面向过程,将可变的部分封装起来,隔离变化,对外提供抽象不变的接口,真正体会到多态的特性所带来的好处,真正做到面向接口编程。

// 接口
public interface MQService{}

// 实现类
public class KafkaMQService implements MQService{}

// 实现类
public class RocketMQService implements MQService{}

// 接口
public interface CacheService{}

// 实现类
public class RedisService implements CacheService{}

// 实现类
public class MemcachedService implements CacheService{}

3.3 里式替换原则

子类对象可以替换父类对象,并不影响原来的业务逻辑。里式替换原则强调的是子类无差异替换父类,应当按照协议来设计,父类定义标准协议,子类改变的是内部实现逻辑,但不改变协议本身的约定。

class A{
    public void sendMessage(String message){
        mqService.send(message);
    } 
}

class B extends A{
    @Override
    public void sendMessage(String message){
        if(message.length() > 200){
            // 压缩
            byte[] msgBytes = compress(message);
            mqService.send(msgBytes);
            return;
        }
        mqService.send(message);
    } 
}

class Demo{
    public void method(A a){
        a.sendMessage("abc");
    }
}

class Main{
    public static void main(String[] args) {
        Demo demo = new Demo();
        demo.method(new A());
        // 子类替换父类
        demo.method(new B());
    }
}

这样看起来,就是利用了多态的特性,但多态是面向对象语言的一大特性,而里式替换则是一种设计原则,比如:如果B类改成如下这样,就不符合里式替换设计原则了。

class B extends A{
    @Override
    public void sendMessage(String message){
        if(message.length() > 200){
            // 丢弃
            return;
        }
        mqService.send(message);
    } 
}

class B extends A{
    @Override
    public void sendMessage(String message){
        if(message.length() > 200){
            // 压缩
            try{
                byte[] msgBytes = compress(message);
                mqService.send(msgBytes);
                return;
            }catch(Exception e){
                throw new RuntimeException("...");
            }
            
        }
        mqService.send(message);
    } 
}

第一种不符合是因为修改了业务逻辑,第二种不符合是因为违反了父类协议。

建议:如果子类不能直接替换掉父类,那么建议通过依赖、组合等方式替代。

3.4 接口隔离原则

调用者不应该依赖于他不需要的接口,这条原则主要是让我们注意接口的设计,避免大而全的接口,接口的职责划分应该明确,如果调用者每次只使用部分接口、那很有可能这个接口的设计就不太合理。

public interface UserService{
    User getUserById(String userId);
}

public interface LoginService{
    boolean login(User user);
}

// 不应该用一个实现类实现两个接口
public class UserLoginServiceImpl implements UserService, LoginService{
    // ...
}

注意:接口隔离原则告诫我们要避免大而全的接口,尽量细化接口的功能,以此提供代码的灵活性,但也要注意不要过于细化,导致接口数量过多,理解高内聚、低耦合的意义,只专注为一个模块提供应有的功能(高内聚),暴露尽可能少的方法(低耦合)。

3.5 依赖倒置原则

高层模块不依赖于底层模块,两者应该通过抽象来互相依赖,抽象不要依赖于具体实现,具体实现要依赖于抽象。
所谓高层模块不依赖底层模块指的就是,调用者不依赖于被调用者,就好像JDBC定义的规范一样,各个数据库厂商在开发设计时和具体的业务逻辑并没有任何依赖关系,而是依赖一种抽象,这个抽象实际上就是JDBC规范,JDBC规范不依赖于具体实现。

Servlet规范定义了Servlet接口

public interface Servlet {

    public void init(ServletConfig config) throws ServletException;
    
    public ServletConfig getServletConfig();
   
    public void service(ServletRequest req, ServletResponse res)
    throws ServletException, IOException;
    
    public String getServletInfo();
   
    public void destroy();
}

虽然定义了接口,但并没有让各个Web Server直接去实现,而是又抽象了HttpServlet类,现在Web Server直接依赖HttpServlet类进行扩展。

  • Servlet:高层模块
  • HttpServlet:抽象
  • SpringMVC(DispatcherServlet)、undertow(DefaultServlet):底层模块

3.6 迪米特法则

迪米特法则又叫最少知识原则,该原则的目的是为了减少代码之间的耦合度,减少类与类之间的依赖,减少细节的暴露,使得功能更加的独立。
该原则主要包含如下三个方面:

  • 每个单元对其他单元只拥有有限的知识,而且这些单元是与当前单元有紧密联系的.
  • 每个单元只能与其朋友交谈,不与陌生人交谈.
  • 只与自己最直接的朋友交谈.

3.7 DRY原则

因为描述为:Don‘t Repeat Yourself,在编程中可以理解为不要写重复的代码,比如同样的业务需求,存在多处不同的实现逻辑,这就会造成代码的阅读困难、维护困难。

比如:对于list集合去重,实现方式有很多。

通过Set特性实现

public static <T> List<T> distinct(List<T> list) {
    return new ArrayList<>(new LinkedHashSet<>(list));
}

通过Java8提供的stream特性实现

public static <T> List<T> distinct(List<T> list) {
    return list.stream().distinct().collect(Collectors.toList());
}

在没有明显的性能差异时,使用哪种方式都可以,但请选择其中一种,不要采用多种方法实现同样的业务逻辑,例子比较简单,你可能还感触不深,试想,如果这是一段复杂的业务逻辑,但却有多种不同的实现方式,肯定会让阅读的人感到很诧异,他肯定会想,这几种实现方式到底有什么不同呢?如果要修改业务逻辑,到底需要改几处呢?还有没有其他被遗漏的地方?

3.8 KISS原则

KISS 是英文 Keep it Simple and Stupid的缩写,意思就是保持简单和愚蠢,这个原则在很多行业都适用,比如产品设计要保持简单,企业管理要保持简单,化繁为简。
实践在编码中,就是要保持代码的可读性和可维护性,让代码容易读懂,容易修改。

何为简单?

  1. 尽量不要使用一些冷门的、奇淫技巧的方式来实现你的业务逻辑。(类似:某种高级语法、正则表达式等)
  2. 善于使用已有的工具,不需要重复造轮子。
  3. 不要过度优化,比如用位运算替代算术方法,简单的业务逻辑,还要使用设计模式。

总结

设计原则是一块比较难以理解与统一的编程规范,因为它不像其他规范一样,都有明确的准则、模板、公式等,设计原则更多的是理解,不同的人理解可能不一样,一段代码,有些人可能认为已经符合设计原则了,有些人则认为不符合,有些人认为封装的合适,有些人则认为是过度封装,所以对于设计原则更重要的应该是保持团队中的认知统一,让团队中的成员保持统一的衡量尺度,不断地提高团队的整体认知水平,这样才能充分发挥出设计原则的作用。

四、设计模式

留白,相关的文章已经够多了。

五、日常踩坑

5.1 避免不必要的对象创建

为了减少创建对象带来的消耗,无论是时间上还是空间上,只要创建对象的成本很高,就应该思考如何能够避免这样的事情,下面我们来看看常见的做法都有哪些?

5.1.1 不可变的对象

最为典型的案例就是String,我想应该不会有人去通过new的方式再去构建一个String字符串了吧!

String str = new String("abc");
String str = "abc";

上面两种方式的区别不用多说,String被设计为不可变对象的好处就在于,在整个JVM内存中,只要遇到相同的字符串字面常量都可以被重用。
String对象的创建成本虽然不高,但却是被频繁使用最多的一种对象了。

5.1.2 静态方法

使用静态对象、静态工厂等方式可以避免重复创建对象。

静态对象

Boolean.valueOf("true");

public static Boolean valueOf(String s) {
    return parseBoolean(s) ? TRUE : FALSE;
}


public static final Boolean TRUE = new Boolean(true);

public static final Boolean FALSE = new Boolean(false);

静态工厂(单例模式)

public class StaticSingleton {
    private static class StaticHolder {
        public static final StaticSingleton INSTANCE = new StaticSingleton();
    }

    public static StaticSingleton getInstance() {
        return StaticHolder.INSTANCE;
    }
}

枚举

public enum EnumSingleton {
    INSTANCE;
}

5.1.3 视图

视图是返回引用的一种方式。

map的keySet方法,实际上每次返回的都是同一个对象的引用。

map.keySet()

public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}

当然返回的对象是不能修改的,否则就会报错

Set<String> sets = map.k
sets.add("xxx");

Exception in thread "main" java.lang.UnsupportedOperationException
    at java.util.AbstractCollection.add(AbstractCollection.java:262)
    at com.example.demo.Immutable.main(Immutable.java:22)

java.util包下的Collections类,还有很多类似的方法。

5.1.4 自动装箱

// 变量定义为Long在我的机器运用需要5秒多,而定义为long则只需要500毫秒左右。
// long sum = 0l;
Long sum = 0l;
for(long i = 0; i<Integer.MAX_VALUE; i++){
    sum += i;
}

5.2 内存泄漏

内存泄漏一般都是存在一些隐蔽的引用行为。

5.2.1 过期引用

下面代码是JDK中提供的Stack类,其中调用元素弹出栈的一段逻辑,看看最后一行,主动的将出栈元素设置为null,作者也还特别注释了其用以,就是为了能够让gc回收它。

public synchronized void removeElementAt(int index) {
    modCount++;
    if (index >= elementCount) {
        throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                 elementCount);
    }
    else if (index < 0) {
        throw new ArrayIndexOutOfBoundsException(index);
    }
    int j = elementCount - index - 1;
    if (j > 0) {
        System.arraycopy(elementData, index + 1, elementData, index, j);
    }
    elementCount--;
    elementData[elementCount] = null; /* to let gc do its work */
}

5.2.2 长生命周期引用短生命周期

典型的代表就是ThreadLocal,一旦Key被回收后,就无法再访问到,但却一直会有另外一条引用链,而这条引用链的生命周期与线程一致,当线程是从线程池中获取时,它将永存。

5.2.3 静态数据、缓存

静态数据也是需要注意的地方,尤其是集合这样的静态数据,本地缓存有时候也就是静态数据,一般都会为其设计过期策略、缓存容量。

5.3 覆盖equals时总要覆盖hashCode

这是一条Object的规范。

  • 在一个java应用程序执行期间,只要对象的equals方法没有被修改,那么对同一个对象的多次调用,hashCode都应该返回同一个值,在一个应用程序与另一个应用程序执行过程中,执行hashCode方法返回的方法可以不一致。
Whenever it is invoked on the same object more than once during
an execution of a Java application, the {@code hashCode} method
must consistently return the same integer, provided no information
used in {@code equals} comparisons on the object is modified.
This integer need not remain consistent from one execution of an
application to another execution of the same application.
  • 如果两个对象equals方法相等,则hashCode方法返回的结果也必须相等。
If two objects are equal according to the {@code equals(Object)}
method, then calling the {@code hashCode} method on each of
the two objects must produce the same integer result.
  • 如果两个对象equals方法不相等,则hashCode方法返回的结果不一定不相等,但让不相等的对象产生不相等的hashCode值,有可能提高hashTabel的性能。
It is <em>not</em> required that if two objects are unequal
according to the {@link java.lang.Object#equals(java.lang.Object)}
method, then calling the {@code hashCode} method on each of the
two objects must produce distinct integer results.  However, the
programmer should be aware that producing distinct integer results
for unequal objects may improve the performance of hash tables.

如果User对象,只重写了equals方法

@Data
class User {
    private String name;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(name, user.name);
    }

}

放入HashMap中,看看执行结果。

Map<User, String> map = new HashMap<>();
User user1 = new User();
user1.setName("zs");

map.put(user1, "zs");

User user2 = new User();
user2.setName("zs");

System.out.println(user1.equals(user2)); // 执行结果:true
System.out.println(map.get(user1)); // 执行结果:zs
System.out.println(map.get(user2)); // 执行结果:null

当把hashCode方法也重写了

@Data
class User {
    private String name;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(name, user.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
}

执行结果正常

System.out.println(map.get(user2)); // 执行结果:zs

原因分析

调用key的hashCode方法得到一个int值

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

优先根据hash值比较,hash值不同直接就返回null了

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

5.4 clone

数据类型

在Java中,数据类型可以分为:基础类型和引用类型,其中当基础类型为全局变量时存储在栈中,为局部变量时存储在堆内存中,无论是在栈中还是在堆中存储的都是具体的值,与之不同的引用类型,则记录的是地址,然后通过引用的方式指向具体的内存区域。
比如在m这个方法中,用到的基础类型a与引用类型User在内存中的存储就如下图所示

public void m(){
    int a = 128;
    User user = new User();
}

赋值语句

在应用程序中对象拷贝一般都可以通过赋值语句来实现,比如像下面这样

int a = 128;
int b = a;

可以认为,b拷贝了a
对于基础类型来说,这样是没有问题的,但对于引用类型就有问题了,比如像下面这样

User user1 = new User();
User user2 = user1;

无论是user1还是user2,只要有一个属性发生了变化,两个对象就都会改变,这通常不是我们希望看到的结果。
基础类型的赋值,实际上在栈中是两个对象

而引用类型的赋值,实际上只是在引用上做了处理,实际在堆中的对象还是只有一个。

深拷贝和浅拷贝

概念理解

  • 浅拷贝:如果是基础类型,则直接拷贝数值,然后赋值给新的对象,如果是引用类型,则只复制引用,并不复制数据本身。
  • 深拷贝:如果是基础类型,和浅拷贝一样,如果使用引用类型,则不是只复制引用,还会复制数据本身。
    深拷贝

不可变对象

有一类对象比较特殊,它们虽然是引用类型对象,但依然可以保证浅拷贝后,得到的就是你想要的对象,那就是不可变对象。
比如像下面这样,str1和str2两个对象是不会互相影响的。

String str1 = "a";
String str2 = str1;

或者是这样的类

final class User {
    final String name;
    final String age;
    public User(String name, String age) {
        this.name = name;
        this.age = age;
    }
}

对于不可变的类,就算直接赋值了又能怎么样,反正你也无法再修改它了,所以它是安全的。

User u1 = new User("小明", "18");
User u2 = u1;

Cloneable接口

实际上JDK也为我们提供了对象clone的方法,就是实现Cloneable接口,只要实现了这个接口的类就表明该对象具有允许clone的能力,Cloneable接口本身不包含任何方法,它只是决定了Object中受保护的clone方法实现的行为:
如果一个类实现了Cloneable接口,Object的clone方法就返回该对象的拷贝,否则就抛出java.lang.CloneNotSupportedException异常。

@Data
@AllArgsConstructor
@NoArgsConstructor
class User implements Cloneable {
    private String name;
    private int age;

    /**
     * 如果没有实现Cloneable接口,调用super.clone()方法就会抛出异常
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

如果你认为一个类实现了Cloneable接口,并且调用super.clone()方法就能够得到你想要的对象,那你就错了,因为super.clone()方法就和浅拷贝一样,如果克隆的对象中包含可变的引用类型,实际上是存在问题的。
只含有基础类型和不可变类型时

public static void main(String[] args) throws CloneNotSupportedException {
    User u1 = new User("小明", 18);
    User u2 = (User) u1.clone();
    
    u2.setName("小王");
    u2.setAge(20);
    
    u1.setName("小红");
    u1.setAge(19);
    
    log.info("u1:{}", u1);
    log.info("u2:{}", u2);
}

因为User对象只有基础类型int和不可变类型String,所以直接调用spuer.clone()方法没有问题

u1:User(name=小红, age=19)
u2:User(name=小王, age=20)

含有引用类型的问题

现在我们为User对象新增一个Role的属性

@Data
@AllArgsConstructor
@NoArgsConstructor
class User implements Cloneable {
    private String name;
    private int age;
    private Role[] roles;

    /**
     * 如果没有实现Cloneable接口,调用super.clone()方法就会抛出异常
     *
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
class Role{
    private String roleName;
}

public static void main(String[] args) throws CloneNotSupportedException {
    User u1 = new User();
    u1.setName("小明");
    u1.setAge(18);
    
    Role[] roles = new Role[2];
    roles[0] = new Role("A系统管理员");
    roles[1] = new Role("B系统普通员工");
    u1.setRoles(roles);
    log.info("u1:{}", u1);
    
    User u2 = (User) u1.clone();
    u2.setName("小王");
    u2.setAge(20);
    
    Role[] roles2 = u2.getRoles();
    roles2[0] = new Role("A系统普通员工");
    roles2[1] = new Role("B系统管理员");
    u2.setRoles(roles2);
    
    log.info("u1:{}", u1);
}

问题出现了,我只修改了克隆出来的u2对象,但是u1对象也没改变了。

u1:User(name=小明, age=18, roles=[Role(roleName=A系统管理员), Role(roleName=B系统普通员工)])
u1:User(name=小明, age=18, roles=[Role(roleName=A系统普通员工), Role(roleName=B系统管理员)])

解决引用类型的问题

典型的浅拷贝的问题,那么要解决这个问题也很简单,改成下面这样即可

@Data
@AllArgsConstructor
@NoArgsConstructor
class User implements Cloneable {
    private String name;
    private int age;
    private Role[] roles;

    /**
     * 如果没有实现Cloneable接口,调用super.clone()方法就会抛出异常
     *
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        User user = (User) super.clone();
        user.roles = roles.clone();
        return user;
    }
}

此时再执行,结果就正确了。

u1:User(name=小明, age=18, roles=[Role(roleName=A系统管理员), Role(roleName=B系统普通员工)])
u1:User(name=小明, age=18, roles=[Role(roleName=A系统管理员), Role(roleName=B系统普通员工)])

问题延伸

实际上在有些的情况下,上面的处理方式还是存在问题,比如像下面这样:
现在对象是HashMap了

@Data
@AllArgsConstructor
@NoArgsConstructor
class User implements Cloneable {
    private HashMap<String, Role> roleMap;
    
    @Override
    protected Object clone() throws CloneNotSupportedException {
        User user = (User) super.clone();
        user.roleMap = (HashMap<String, Role>) roleMap.clone();
        return user;
    }
}

public static void main(String[] args) throws CloneNotSupportedException {
    User u1 = new User();
    
    HashMap<String, Role> roleMap1 = new HashMap<>();
    roleMap1.put("A", new Role("系统管理员"));
    u1.setRoleMap(roleMap1);
    log.info("u1:{}", u1);
    
    User u2 = (User) u1.clone();
    HashMap<String, Role> roleMap2 = u2.getRoleMap();
    Role role = roleMap2.get("A");
    role.setRoleName("普通员工");
    roleMap2.put("A", role);
    u2.setRoleMap(roleMap2);
    
    log.info("u1:{}", u1);
}

u1:User(roleMap={A=Role(roleName=系统管理员)})
u1:User(roleMap={A=Role(roleName=普通员工)})

为什么不行呢?因为HashMap提供的克隆方法本身就是浅拷贝。

 /**
  * Returns a shallow copy of this HashMap instance: the keys and
  * values themselves are not cloned.
  *
  * @return a shallow copy of this map
  */
 @SuppressWarnings("unchecked")
 @Override
 public Object clone() {
     HashMap<K,V> result;
     try {
         result = (HashMap<K,V>)super.clone();
     } catch (CloneNotSupportedException e) {
         // this shouldn't happen, since we are Cloneable
         throw new InternalError(e);
     }
     result.reinitialize();
     result.putMapEntries(this, false);
     return result;
 }

最终的解决方式

字节流

你在百度上很容易查询到解决方式,最常见的就是字节流。
比如像下面这样。

@Data
@AllArgsConstructor
@NoArgsConstructor
class User implements Serializable {
    private HashMap<String, Role> roleMap;

    public static <T extends Serializable> T clone(T obj) {
        T cloneObj = null;
        try {
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
            outputStream.writeObject(obj);
            outputStream.close();

            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
            ObjectInputStream inputStream = new ObjectInputStream(byteArrayInputStream);
            cloneObj = (T) inputStream.readObject();
            inputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return cloneObj;
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
class Role implements Serializable {
    private String roleName;
}

此时再调用就没有问题了。

public static void main(String[] args) {
    User u1 = new User();
    HashMap<String, Role> roleMap1 = new HashMap<>();
    roleMap1.put("A", new Role("系统管理员"));
    u1.setRoleMap(roleMap1);
    log.info("u1:{}", u1);
    
    User u2 = User.clone(u1);
    HashMap<String, Role> roleMap2 = u2.getRoleMap();
    Role role = roleMap2.get("A");
    role.setRoleName("普通员工");
    roleMap2.put("A", role);
    u2.setRoleMap(roleMap2);
    
    log.info("u1:{}", u1);
}

u1:User(roleMap={A=Role(roleName=系统管理员)})
u1:User(roleMap={A=Role(roleName=系统管理员)})
重新实现

实际上你可以自己实现一套clone方法,给它定义为拷贝工厂,或者使用一些已经实现好的第三方工具类。
比如org.springframework.beans包下提供的BeanUtils类

public static void main(String[] args) {
    User u1 = new User();
    HashMap<String, Role> roleMap1 = new HashMap<>();
    roleMap1.put("A", new Role("系统管理员"));
    u1.setRoleMap(roleMap1);
    log.info("u1:{}", u1);
    
    User u2 = new User();
    // 使用copyProperties方法
    BeanUtils.copyProperties(u1,u2);
    HashMap<String, Role> roleMap2 = u2.getRoleMap();
    Role role = roleMap2.get("A");
    role.setRoleName("普通员工");
    roleMap2.put("A", role);
    u2.setRoleMap(roleMap2);
    
    log.info("u1:{}", u1);
}
Hutool工具包
// 命名几乎和spring的一样
BeanUtil.copyProperties(u1, u2);
总结

实际上你应该已经发现了,虽然Object类为我们提供了clone方法,但有时候并不能很好的使用它,可能需要多层级的逐个克隆,甚至如果添加了某个引用对象时,忘了修改clone方法还会带来一些奇怪的问题,也许我们应该永远不去使用它,而是通过其他的方式来替代。

5.5 Null值处理

5.5.1 字符串

很多工具包中都有对字符串空值的处理。

// guava包
if(Strings.isNullOrEmpty(str)){
    
}

// apache.commons.lang包,空格也算空
if(StringUtils.isBlank(str)){
    
}

// apache.commons.lang,空格不算空
if(StringUtils.isEmpty(str)){
    
}

guava中还有对Null与Empty的转换

// null转换为""
Strings.nullToEmpty(str);
// ""转换为null
Strings.emptyToNull(str);

有些方法的默认对null值的处理也要注意,比如String.valueOf(null)默认就会返回"null"字符串。

String.valueOf(null); // 结果 "null"

5.5.2 Objects

JDK1.7开始提供的Objects类,处理Object中的一些方法,尤其是对于Null值的处理
equals方法不用再担心null值的问题了。

// 可能会遇到java.lang.NullPointerException异常
if(str.equals("abc")){
    
}
// 完全不用担心
Objects.equals(str,"abc");

// 已经对空值做了处理
public static boolean equals(Object a, Object b) {
    return (a == b) || (a != null && a.equals(b));
}

hashCode方法也一样

String str = null;
// NullPointerException异常
str.hashCode();
// 返回0
int i = Objects.hashCode(str);

public static int hashCode(Object o) {
    return o != null ? o.hashCode() : 0;
}

toString方法

A a = null;
// NullPointerException异常
a.toString();
// 返回"null"
Objects.toString(a);
// 还可以指定替代值,返回"abc"
Objects.toString(a, "abc");

各种null值的检查、处理

Object obj = null;
// NullPointerException异常
Objects.requireNonNull(obj);
// NullPointerException异常,异常message可以自定义
Objects.requireNonNull(obj,"obj is null");
// 返回true
System.out.println(Objects.isNull(obj));
// 返回false
System.out.println(Objects.nonNull(obj));

5.5.3 集合的处理

Map空Value处理

每次从Map中根据Key值获取Value值时,总要注意Null值的情况。

一种简单的通过Map来计数的功能。

Map<String, Integer> map = new HashMap<>();
if (map.containsKey("a")) {
    int i = map.get("a");
    map.put("a", i + 1);
} else {
    map.put("a", 1);
}

使用getOrDefault替换后,一行代码,更加简洁。

map.put("a", map.getOrDefault("a", 0) + 1);

在与Json格式做转换时

Map<String, Object> map = new HashMap<>();
map.put("a",null);
String jsonString = JSON.toJSONString(map, SerializerFeature.WRITE_MAP_NULL_FEATURES, SerializerFeature.QuoteFieldNames);
System.out.println(JSON.toJSONString(map));// 返回:{}
System.out.println(jsonString);// 返回:{"a":null}

返回空集合

List<User> users = getUsers();
if(users != null){
    for(User user : users){
        // ...
    }
}

如果返回对象是一个集合,那么不要返回null,请使用空集合来替代。

Collections.emptyList();
Collections.emptyMap();
// 现在可以放心使用了
List<User> users = getUsers();
for(User user : users){
    // ...
}

5.5.4 避免传递Null值

传递Null值和返回Null值一样,都需要做额外的处理,很容易导致NEP异常的产生。

// 用户null做业务逻辑区分,那么代码就会变成下面这样,不但增加了复杂度,如果入参在多处地方使用,还很容易遗漏
public void test(User user, Address address){
    
    if(user == null){
        // ...
    }else{
        
    }
    
    if(address == null){
        // ...
    }else{
        
    }
}

// 如果方法不支持null值,还需要做额外的校验处理,并产生异常
public void test(User user, Address address){
    assert user != null : "user should not be null";
    assert address != null : "user should not be null";
}

5.5.5 Optional

Optional是一个容器对象,可以存储各种类型的值,包括Null,Optional针对对象为Null值的情况做了很多处理,一起来看看吧!

构建Optional对象

Optional<String> optional = Optional.of("abc");
System.out.println(optional);// 输出:Optional[abc]

of方法构建一个泛型Optional对象

public static <T> Optional<T> of(T value) {
    return new Optional<>(value);
}

如果对象为Null,of方法会抛出异常

public static <T> Optional<T> of(T value) {
    return new Optional<>(value);
}

private Optional(T value) {
    this.value = Objects.requireNonNull(value);
}

public static <T> T requireNonNull(T obj) {
    if (obj == null)
        throw new NullPointerException();
    return obj;
}

当然Optional也支持通过ofNullable方法构建Null对象

Optional<String> optional = Optional.ofNullable(null);
System.out.println(optional);// 输出:Optional.empty

然过get方法可以得到泛型对象

Optional<String> optional = Optional.of("abc");
System.out.println(optional.get()); // 输出 abc

如果对象为Null,get方法会抛出异常

public T get() {
    if (value == null) {
        throw new NoSuchElementException("No value present");
    }
    return value;
}

通过isPresent方法判断对象是否为Null

public boolean isPresent() {
    return value != null;
}

System.out.println(Optional.ofNullable("abc").isPresent()); // 输出:true
System.out.println(Optional.ofNullable(null).isPresent()); // 输出:false
// ifPresent可以接收Consumer类型
Optional.of("abc").ifPresent(sb -> System.out.println(sb.toUpperCase()));// 输出:ABC

orElse方法,提供了对Null值对象的处理

System.out.println(Optional.ofNullable(null).orElse("abc"));// 输出:abc
System.out.println(Optional.empty().orElse("abc"));/// 输出:abc

orElseGet方法,支持传入Supplier类型

System.out.println(Optional.ofNullable(null).orElseGet(() -> "null"));// 输出:"null"

或者使用orElseThrow方法抛出异常

Optional.empty().orElseThrow(() -> new Exception("test exception"));

5.5.6 JDK14改进

对于下面这段代码,如果a为null,则会抛出NEP异常,并且通过异常堆栈信息,能够准确地定位具体的代码行数,以此来确定是a对象为null导致的NEP异常。

a.i = 0;

但假设是下面这段逻辑呢?如果异常堆栈信息只能精确到具体的行数,那就无法确定到底是a、b、c中哪一个对象为null导致的异常。

a.b.c = 0;

类似的场景还有很多

a.i = b.i;
a().b().i = 0;
arr[a][b] = 0;

JDK14对此做了改进,可以明确地输出导致NEP异常的对象,就像如下这样。

Exception in thread "main" java.lang.NullPointerException: 
        Cannot read field "c" because "a.b" is null
    at Prog.main(Prog.java:5)

5.6 遇到的一些坑

Number类的缓存

对象比较还得使用equals,除了Integer之外,Character、Byte、Short、Long都有类似的设计。

Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1 == i2); // true
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4); // false

BigDecimal的精度

注意BigDecimal构造方法传入double类型的值,存在精度问题。

BigDecimal bigDecimal = new BigDecimal(0.1);
System.out.println(bigDecimal.toString()); // 结果:0.1000000000000000055511151231257827021181583404541015625
BigDecimal bigDecimal2 = new BigDecimal("0.1");
System.out.println(bigDecimal2.toString()); // 结果:0.1

BigDecimal除不尽

使用divide方法时,如果结果除不尽,就会抛出异常,所以无论如何都应该指定保留的小数点位数。

BigDecimal d1 = new BigDecimal("10");
BigDecimal d2 = new BigDecimal("3");
System.out.println(d1.divide(d2));

Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

BigDecimal比较

BigDecimal的比较,请使用compareTo方法

BigDecimal d1 = new BigDecimal("1");
BigDecimal d2 = new BigDecimal("1.0");
System.out.println(d1.equals(d2)); // 结果:false,应替换为d1.compareTo(d2)

String.valueOf处理null值

String.valueOf()传入参数是null值时,会返回null值字符串,可能导致某些判空的场景出现问题。

Object obj = null;
String str = String.valueOf(obj);
system.out.println(str); // 结果:"null"

public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

Integer.parseInt转换

String str = "1.0";
int i = Integer.parseInt(str); // 这会直接报错,java.lang.NumberFormatException: For input string: "1.0"
// 改成这样即可
int i1 = new BigDecimal(str).intValue();

集合的UnsupportedOperationException

// Collections中提供的emptyMap、emptySortedMap、emptyList、emptySet等返回的对象,都是不可操作的
Map<String, String> m1 = Collections.emptyMap();


// singletonMap、singletonList、singleton等返回的对象,都是不可操作的
List<String> singletonList = Collections.singletonList("a");


// Map中的keySet、entrySet、values,都是不可操作的
Map<String, String> m2 = new HashMap<>();


// asList返回的是不可操作的
Arrays.asList(list);

你可能感兴趣的:(经验分享,java,代码规范,后端)