常犯指数4颗星- 计算、集合、接口

发布时间 2023-06-13 23:56:26作者: shine-rainbow

Java计算、集合、集合接口

总是用不好数值计算、日期计算该怎么办

用于精确计算的类BigDecimal

image-20230613204514635

  • BigDecimal核心是精度,精度如果不匹配,结果大概率不会符合预期

初始精度需要匹配

/**
  * Scala 需要与小数位匹配
  */
private static void scaleProblem() {
    BigDecimal decimal = new BigDecimal("12.222");
    //制定小数精度不够, 底层会自动补零 12.222000000000
    //        BigDecimal result1 = decimal.setScale(12);
    //        System.out.println(result1);
    //指定小数精度丢失场景
    //        BigDecimal result2 = decimal.setScale(2);
    //        System.out.println(result2);
    // 使用两个参数的setScale方法,(newScala,roundingMode)
    BigDecimal result = decimal.setScale(2, RoundingMode.HALF_UP);
    System.out.println(result);
}

除法结果需要精度

/**
  * BigDecimal 做除法时出现除不尽的情况
  */
private static void divideProblem() {
    // 除不尽, 未传递roundingMode,抛出异常
    System.out.println(new BigDecimal(30).divide(new BigDecimal(7)));
    // 可以正确执行
    System.out.println(new BigDecimal(30).divide(new BigDecimal(7), 2, RoundingMode.HALF_UP));
}

数值比较需要精度匹配

/**
  * 精度问题导致比较结果和预期不一致
  */
private static void equalProblem() {
    BigDecimal bd1 = new BigDecimal("0");
    BigDecimal bd2 = new BigDecimal("0.0");
    //false
    System.out.println(bd1.equals(bd2));
    // true
    System.out.println(bd1.compareTo(bd2) == 0);
}

equals返回为false, 是因为在equals源码中,如果精度不一致,会直接返回false

image-20230613210653767

SimpleDateFormat使用上的坑

image-20230613210848434

  • 可以解析大于/等于它定义的时间精度,但是不能解析小于它定义的时间精度
/**
  * SimpleDateFormat 可以解析大于/等于它定义的时间进度
  *
  * @throws Exception
  */
private static void formatPrecision() throws Exception {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    String time_x = "2020-03-01 00:00:00";
    String time = "2020-03";
    System.out.println(sdf.parse(time_x));
    // 抛出异常  Unparseable date: "2020-03"
    System.out.println(sdf.parse(time));
}
  • 它是线程不安全的,在多线程环境下操作,会抛异常
 /**
   * SimpleDataFormat 存在线程安全问题
   */
private static void threadSafety() {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 100, 1,
                                                                   TimeUnit.MINUTES, new LinkedBlockingDeque<>(1000));

    while(true){
        threadPoolExecutor.execute( ()->{
            String dateString = "2020-03-01 00:00:00";
            try{
                Date parseDate  = sdf.parse(dateString);
                String dataString2 = sdf.format(parseDate);
                System.out.println(dateString.equals(dataString2));
            }catch (ParseException ex){
                ex.printStackTrace();
            }

        });
    }

}

由于调用的format方法内部有引用canlendar对象

image-20230613212633440

canlendar 本身是线程不安全的,如下图,因此导致SimpleDataFormat format()线程不安全。详细见博客SimpleDateFormat的线程不安全问题

重点放在 calendar ,这个 format 方法在执行过程中,会操作成员变量 calendar 来保存时间 calendar.setTime(date) 。

但由于在声明 SimpleDateFormat 的时候,使用的是 static 定义的,那么这个 SimpleDateFormat 就是一个共享变量,SimpleDateFormat 中的 calendar 也就可以被多个线程访问到,所以问题就出现了,举个例子:

假设线程 A 刚执行完 calendar.setTime(date) 语句,把时间设置为 2020-09-01,但线程还没执行完,线程 B 又执行了 calendar.setTime(date) 语句,把时间设置为 2020-09-02,这个时候就出现幻读了,线程 A 继续执行下去的时候,拿到的 calendar.getTime 得到的时间就是线程B改过之后的。

除了 format() 方法以外,SimpleDateFormat 的 parse 方法也有同样的问题。

至此,我们发现了 SimpleDateFormat 的弊端,所以为了解决这个问题就是不要把 SimpleDateFormat 当做一个共享变量来使用。
————————————————
版权声明:本文为CSDN博主「Archie_java」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_43842093/article/details/125108071

image-20230613212850415

解决方案如博客,可以引用锁、apache工具集等。

SimpleDateFormat 讲解](https://www.cnblogs.com/Vincent-yuan/p/14998651.html)

小小 for 循环,粘上集合出大问题

传统的for循环是怎样的

  • 如果是数组:通过数组长度,建立索引
//传统方式 - 使用索引
int [] xyz = new int[]{1,2,3,4,5};
for (int i = 0; i < xyz.length; i++) {
    System.out.println(xyz[i]);
}
  • 如果是集合:迭代器
// 传统方式 - 迭代器
for (Iterator<Integer> i = left.iterator(); i.hasNext();) {
    System.out.println(i.next());
}

传统的for循环存在怎样的弊端与劣势呢

  • 我需要的是可迭代对象的元素,并不需要元素的索引
//嵌套迭代容易出现问题
for (Iterator<Integer> l = left.iterator(); l.hasNext();) {
    for(Iterator<Integer> r= right.iterator();r.hasNext();){
        System.out.println(l.next()*r.next());
    }
}
//
1
4
9
16
25
6
14
Exception in thread "main" java.util.NoSuchElementException
	at java.util.AbstractList$Itr.next(AbstractList.java:364)
	at com.imooc.java.escape.ForeachOptimize.wrongIterator(ForeachOptimize.java:37)
	at com.imooc.java.escape.ForeachOptimize.main(ForeachOptimize.java:45)

Process finished with exit code 1

// 正确的用法,嵌套迭代
for (Iterator<Integer> l = left.iterator(); l.hasNext();) {
    Integer tmp = l.next();
    for(Iterator<Integer> r= right.iterator();r.hasNext();){
        System.out.println(tmp * r.next());
    }
}
  • 在嵌套环境下(多个可迭代对象),需要小心迭代器对象的正确性

for-each优于for

  • 只专注于迭代对象自身,而不考虑多余的索引
  • 任何实现Iterable接口的对象,都可以使用for-each循环处理
  • 进一步扩展:java8 Iterable.forEach

image-20230613215643481

private static void square(int value){
    System.out.println(value * value);
}

public static void main(String[] args) {
    // wrongIterator();
    // Java8 Iterable.forEach vs for-each
    for(Integer l :left){
        square(l);
    }

    left.forEach( l ->square(l));

    left.forEach(ForeachOptimize::square);
};

如果不好好判定,集合存储就会乱套

Object的equals和hashCode方法

  • 默认的equals和hashCode实现是怎样的(Object中定义的)?
  • 只重写equals方法真的会出错嘛?

集合中元素的索引竟然也与equals方法相关

  • 类实现了compareTo方法,就需要实现equals方法
  • compareTo与equals的实现过程需要同步

image-20230613223016202

 public static class User implements Comparable<User>{
        private String name;
        private Integer age;

        public User() {
        }

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

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Integer getAge() {
            return age;
        }

        public void setAge(Integer age) {
            this.age = age;
        }

        @Override
        public boolean equals(Object obj){
            if(obj instanceof User){
                User user = (User) obj;
                return this.name.equals(user.name) && this.age == user.age;
             }
            return false;
        }

        @Override
        public int hashCode(){
            int result = name.hashCode();
            result = 31 * result +age;
            return result;
        }

        @Override
        public int compareTo(User o) {
            return (this.age - o.age) + this.name.compareTo(o.name);
        }
    }

在main方法中定义set和map分别存储元素,如果未重写hashCode,则返回两个元素,尽管两个元素一模一样

 private static void equalsAndHashcode(){
        User user1 = new User("qinyi",19);
        User user2 = new User("qinyi",19);

//      System.out.println(user1.equals(user2));
        // set判断对象是按照对象的hashCode是否一致
        Set<User> userSet = new HashSet<>();
        userSet.add(user1);
        userSet.add(user2);


        Map<User,Integer> userIntegerMap = new HashMap<>();
        userIntegerMap.put(user1,0);
        userIntegerMap.put(user2,0);

        System.out.println(userSet.size());
        System.out.println(userIntegerMap.size());

   }

如果定义元素索引,通过list.indexOf()和Collections.binarySearch()得到的结果是不一致的,原因是indexOf()是给予equals方法查找的,而binarySearch是基于compareTo方法查找,需要将compareTo和equals实现过程同步。

/**
  * 集合元素索引与 equals方法相关
  */
private static void compareToAndEquals(){
    List<User> users = new ArrayList<>();
    users.add(new User("qinyi",10));
    users.add(new User("qinyi",20));

    User user = new User("qinyi",20);

    int index1 = users.indexOf(user);
    int index2 = Collections.binarySearch(users,user);
    // 0  基于equals方法进行查找
    System.out.println(index1);
    // 1  基于compareTo方法进行查找
    System.out.println(index2);
}

使用lombok注解没有达到预期该怎么办

image-20230613225640681

image-20230613231922396

利用抽象语法树在编译时将注解转换为部分代码生成字节码文件

  • 坑1:无法正确序列化对象
		<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.18</version>
        </dependency>
        
        <!-- 引入 jackson -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.10.0</version>
        </dependency>
@Data
public class Personal {
    private String iPhone;
    private String name;
    private String userName;
}

对json字符串{\"name\":\"qinyi\",\"userName\":\"qinyi-imooc\",\"iPhone\":\"8.1\"} ,进行反序列化会报错找不到属性iPhone

 private static void singleAlphabetHump() throws Exception{
        ObjectMapper mapper = new ObjectMapper();
        Personal personal = new Personal();
        personal.setIPhone("8.1");

//      System.out.println(mapper.writeValueAsString(personal));
        String json = "{\"name\":\"qinyi\",\"userName\":\"qinyi-imooc\",\"iPhone\":\"8.1\"}";
        Personal personal1 = mapper.readValue(json,Personal.class);
        System.out.println(personal1);
    }

image-20230613232419209

原因是因为iPhone属性不是标准的驼峰命名,而lombok会将其转换为iphone进行反序列化,验证将json字符串改为{\"name\":\"qinyi\",\"userName\":\"qinyi-imooc\",\"iphone\":\"8.1\"},报错消失,

image-20230613232559591

因此需要在进行属性命名时采用驼峰命名,避免出现单字母驼峰命名

坑2:对象比较未比较父类的属性值

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Computer {
    private Integer id;
    private String name;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AppleComputer extends Computer{
    private long price;
    private String color;
    public AppleComputer(Integer id,String name,long price,String color){
        super(id,name);
        this.price = price;
        this.color = color;
    }
}

测试代码

 /**
   * lombok第二个坑
   */
private static void equalsAndHashCodeBug(){
    AppleComputer computer1 = new AppleComputer(1,"Mac Pro",1L,"yellow");
    AppleComputer computer2 = new AppleComputer(2,"Mac Air",1L,"yellow");
    System.out.println(computer1.equals(computer2));
}

输出结果为true,查看lombok生成的编译后的字节码文件,可以看到,equals方法只比较的子类属性值

image-20230613233202466

  1. callSuper = true,根据子类自身的字段值和从父类继承的字段值 来生成hashcode,当两个子类对象比较时,只有子类对象的本身的字段值和继承父类的字段值都相同,equals方法的返回值是true。

  2. callSuper = false,根据子类自身的字段值 来生成hashcode, 当两个子类对象比较时,只有子类对象的本身的字段值相同,父类字段值可以不同,equals方法的返回值是true。

因此将@EqualsAndHashCode(callSuper = true)设置为true(默认是false)之后,就可以带上父类属性值进行比较。再次查看编译后的子类字节码。

image-20230613235054905

观察可以发现,equals方法中加入了父类对象的方法比较。

详细解读见博客@EqualsAndHashCode(callSuper = true/false) 作用