Java开发常出错4颗星——计算、集合、接口
Java计算、集合、集合接口
总是用不好数值计算、日期计算该怎么办
用于精确计算的类BigDecimal
- 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
SimpleDateFormat使用上的坑
- 可以解析大于/等于它定义的时间精度,但是不能解析小于它定义的时间精度
/**
* 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对象
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
解决方案如博客,可以引用锁、apache工具集等。
小小 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
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的实现过程需要同步
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注解没有达到预期该怎么办
利用抽象语法树在编译时将注解转换为部分代码生成字节码文件
- 坑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);
}
原因是因为iPhone属性不是标准的驼峰命名,而lombok会将其转换为iphone进行反序列化,验证将json字符串改为{\"name\":\"qinyi\",\"userName\":\"qinyi-imooc\",\"iphone\":\"8.1\"}
,报错消失,
因此需要在进行属性命名时采用驼峰命名,避免出现单字母驼峰命名
坑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方法只比较的子类属性值
callSuper = true,根据子类自身的字段值和从父类继承的字段值 来生成hashcode,当两个子类对象比较时,只有子类对象的本身的字段值和继承父类的字段值都相同,equals方法的返回值是true。
callSuper = false,根据子类自身的字段值 来生成hashcode, 当两个子类对象比较时,只有子类对象的本身的字段值相同,父类字段值可以不同,equals方法的返回值是true。
因此将@EqualsAndHashCode(callSuper = true)
设置为true(默认是false)之后,就可以带上父类属性值进行比较。再次查看编译后的子类字节码。
观察可以发现,equals方法中加入了父类对象的方法比较。
详细解读见博客@EqualsAndHashCode(callSuper = true/false) 作用
怎么避免抽象类和接口选择事务呢
共同的属性用抽象类去表达:如起床、上下班。
特有的属性:用接口去实现
抽象类、接口的含义和特性
- 抽象类是子类的通用特性,包含了属性和行为;接口是定义行为,并不关心谁去实现。
- 抽象类是对类本质的抽象,表达的是is a的关系;接口是对行为的抽象,表达的是like a的关系。
抽象类、接口的相同点
- 接口中的方法(java8改变了这一语法,default 描述)和抽象类中的抽象方法都不能有方法体,并且必须在子类中实现。
- 都可以被继承,但不能被实例化。
抽象类、接口的不同点
- 使用时语法不同,抽象类使用extends,接口则使用implements
- 接口中只能定义常量,所以不能表达对象状态,而接口可以
- 接口中的方法必须时public类型的,而抽象类则没有限制
- 类可以同时实现多个接口(间接解决了Java不支持多继承的难题),但是只能继承一个接口类
为什么接口的默认方法和静态方法改写总不和预期
你是不是没理解函数式接口和lambda表达式就用了
无参场景下用lambda
/**
* java 1.8之前创建线程
*/
private static void baseUse(){
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("anonymous Class Thread run()");
}
}).start();
}
/**
* java8 创建线程 无参数
*/
private static void easyUserLambda(){
new Thread(() -> System.out.println("anonymous Class Thread run()")).start();
}
有参场景下用lambda
/**
* 按照字符串长度进行排序
*/
private static void myCompare(){
List<String> list = Arrays.asList("z", "y", "x", "a");
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
if(s1 == null){
return -1;
}
if(s2 == null){
return 1;
}
return s1.length() - s2.length();
}
});
// java8使用lambda 表达式去实现。有参数
Collections.sort(list,(s1,s2)->{
if(s1 == null){
return -1;
}
if(s2 == null){
return 1;
}
return s1.length() - s2.length();
});
}
自定义函数式接口
/**
*
* 函数式接口有且只有一个方法
*/
@FunctionalInterface
public interface IFindWorker {
Worker findWorkerById(Long id);
}
测试调用
private static final Map<Long,Worker> id2WorkerMap = new HashMap<>();
static {
id2WorkerMap.put(1L,new Worker(1L,"qinyi",19));
}
public static void main(String[] args) {
// IFindWorker findWorker = id -> id2WorkerMap.get(id);
IFindWorker findWorker = id2WorkerMap::get;
System.out.println(findWorker.findWorkerById(1L));
}
/**
* stream和lambda可能导致计算低效
*/
private static void badUseLambda(){
List<String> names = Arrays.asList("qinyi", "imooc");
// 会造成jvm中存储多次中间结果
int longestNameSize = names.stream()
.filter(s->s.startsWith("q"))
.mapToInt(String::length)
.max()
.getAsInt();
// 三个操作放在一个流水线中
int longest = 0;
for (String str: names) {
if(str .startsWith("q")){
int len = str.length();
longest = Math.max(len,longest);
}
}
System.out.println(longest);
}