likes
comments
collection
share

99%的人都踩过坑,全面整理60例Java避坑指南!

作者站长头像
站长
· 阅读数 4

线程安全

Java 语言基础 易错知识点

  1. Integer包装器类型若为 null 值转换为 int 会产生空指针异常。应先判断包装器类型是否为非 null,再转换为基本类型。
  2. 在使用 Double 和 Float 时会丢失精度,例如 1.0 - 0.9 期望是0.1,结果是 0.09999999999999998。因此,在涉及金额计算时,最好将金额转换为整型,例如将 1.1 元表示为 110 分。
double result = 1.0 - 0.9;
System.out.println(result);//0.09999999999999998
@Test
public void test2() {
   System.out.println(String.valueOf(getSimpleString()));
}

public static <T> T getSimpleString() {
   return (T) "121";
}
  1. break默认情况下可以跳出当前层的循环。然而,若需要跳出最外层的循环,我们需要为for循环取一个名字,并且在break语句中指定要跳出哪一层的循环。举个例子,下面的例子中使用了break loop1,用来指定跳出最外层的循环。
int m = 10;
int n = 4;

loop1:
for (int i = 0; i < m; i++) {
    loop2:
    for (int j = 0; j < n; j++) {
        break loop1;
    }
    System.out.println("i:" + i);
}
  1. 内部类中无法直接修改外部引用的值,此时我们可以采用数组作为容器来传递数值。若在内部类中需要修改suc的值,我们可以将其声明为一个数组类型,并通过修改该数组元素的值来实现目的。
int[] suc = new int[1];

new Thread(new Runnable() {
   @Override
   public void run() {
      int i = 0;
      i++;
      suc[0] = i;
   }
}).start();
  1. 为了判断字符串是否相等,我们应该使用String.equals或者Objects.equals方法,而不能简单地使用 == 运算符。

  2. 在处理包装器类型Integer、Long时,不建议使用"=="来判断它们是否相等,最好采用Objects.equals方法来判断相等。因为"=="判断相等的方式是根据对象的内存地址来判断的。(尽管在-127到128的范围内,由于对象池缓存的存在,可以使用"=="进行判等,但为了统一起见,最好还是使用Objects.equals来判断相等)

  3. 为了避免出现乱码,调用String.getBytes(Charset.forName("utf-8"))时应明确指定字符集,因为不同的操作系统环境有不同的默认字符集。

  4. 使用StringUtils.isEmpty判断字符串是否为null,使用StringUtils.isBlank判断字符串是否为空白或者为null。

  5. String是不可变对象,调用substring、replace等方法不会修改原始的String对象。如果需要修改字符串,应该使用线程安全的StringBuilder。在单线程环境下,也可以使用StringBuffer。

  6. 枚举类型的相等判断可以使用"=="。例如,statusEnum == StatusEnum.Success可以判断枚举类型statusEnum是否等于StatusEnum.Success

  7. Java中的基本类型和引用类型都是通过值传递的。因此以下代码尝试交换a、b两个基本类型的值的方法是错误的。

public void testSwap() {
   int a = 1;
   int b = 2;
   swap(a, b); // a、b值不会变化。
}
public void swap(int a, int b) {
   int c = a;
   a = b;
   b = c;
}
  1. 不建议在 boolean 类型的命名中使用 is 前缀,因为 getter 方法默认会自动带上 is 前缀。如果使用 is 前缀,可能会导致 JSON 等解析异常。

  2. 在使用 switch 语句时,只能使用常量,不能使用变量。

  3. 使用 switch case 语句时,记得和 break 关键字搭配使用。执行完一个 case 后的语句后,流程控制会转移到下一个 case 继续执行。如果你只想执行这一个 case 语句,不想执行其他 case,那么就需要在这个 case 语句后面加上 break,以跳出 switch 语句。

  4. 如果要使用 Object.clone 方法,需要确保类型实现了 Cloneable 接口,否则调用 clone 方法会抛出 CloneNotSupportedException 异常。

  5. Object.clone 方法属于浅层拷贝,即基础数据类型会被复制,而引用类型则是共享的。如果需要实现深层拷贝,可以考虑使用 BeanUtils.copyProperties 方法。

  6. 在使用 List 进行 for 循环遍历时,不能在遍历期间直接删除元素。如果需要删除元素,可以使用 stream 表达式进行过滤,或者使用 iterator 删除元素。

Iterator<Integer> it = numbers.iterator();  
        while(it.hasNext()) {  
            Integer i = it.next();  
            if(i < 10) {    
                it.remove();  // 删除小于 10 的元素  
            }  
        }
  1. 在服务正式运行时,为了记录日志,我们不能使用e.printStackTrace()System.out.println打印日志,而是应该使用log4j Logger。因为标准输入输出会被重定向,使用日志框架将请求记录到日志文件中更为合适。

  2. 应该谨慎处理异常捕获,避免直接忽略异常而频繁发生错误。

  3. 对于RuntimeException等异常,我们也应该记录异常的根因。我们应该使用new RuntimeException("构建xxx出现异常", e)的方式记录根异常,而不是仅仅使用new RuntimeException("构建xxx出现异常")。这样的做法可以保留异常堆栈信息,有助于问题的排查。

try{
    //业务逻辑
}catch(Exception e){
    throw new RuntimeException("构建xxx出现异常", e);
}
  1. ErrorException都属于throwable类型。如果在捕获异常时使用catch Exception,那么遇到Error是无法被捕获的。常见的Error有OutOfMemoryErrorNoSuchMethodError等。如果想要捕获Error类型的异常,需要使用catch Throwable,这样就可以捕获到ExceptionError

  2. 当我们使用finally代码块中的return语句时,try代码块中的return语句也会同时执行。最终,返回的值将会是finally代码块中return的值。

@Test
public void testTry() {
    System.out.println("getValue:" + getValue());
}

public int getValue() {
    int i = 0;
    try {
        System.out.println("try");
        i = 1;
        return i;
    } finally {
        i = 2;
        return i;
    }
}

执行结果getValue 返回 2 24. 当try代码块中使用return语句返回i时,无论finally块中如何修改i,都不会影响返回值。

@Test
public void testTry() {
    System.out.println("getValue:" + getValue());
}

public int getValue() {
    int i = 0;
    try {
        System.out.println("try");
        i = 1;
        return i;
    } finally {
        i = 2;
    }
}

getValue()函数的执行结果为1。尽管 finllay 修改了 i 的值,但并不会对 try 模块的返回值产生影响。这是因为返回值在 return 语句执行后会被暂存在栈中,并且后续对其再次的修改也不会对该返回值产生影响,除非在 finllay 中再次使用 return 语句来覆盖返回值。

Jdk和其他类库易错知识点

  1. 在使用HashMap时,我们可能会遇到哈希冲突的问题。因为两个元素可以拥有相同的hashCode,但是它们的equals方法却不能相等。为了解决这个问题,我们需要重写键值的hashCodeequals方法,确保当两个元素相等时,它们的hashCodeequals都应该相等。

  2. HashMap是非线程安全的。如果我们需要在多线程环境下使用HashMap,可以考虑使用ConcurrentHashMap这个线程安全类。

  3. HashMap的values和keySet方法返回的是无序集合。如果我们希望按照插入顺序排序,可以使用LinkedHashMap

  4. 对于Set集合,其中的对象也是无序的,遍历Set集合的结果与插入Set集合的顺序并不相同。如果我们希望有序,可以使用LinkedHashSet

  5. HashMap可以插入key、value都是null的元素,并且containsKey(null)会返回true。这是需要我们在使用HashMap时要注意的一点。

Map<String, String> map = new HashMap();
map.put(null, null);
System.out.println(map.containsKey(null));

containsKey 返回 true

  1. ConcurrentHashMap 的 key 和 value 都不能为 null,否则会出现 NPE 错误。

  2. 当我们使用Arrays.asList方法创建一个List对象时,这个List对象是不允许添加add、清理clear元素的。

  3. 使用ArrayList的subList方法创建子List,这个子List会与父List共享相同的底层存储空间。因此,在子List中添加元素会对父级List产生影响,即父级List也会被修改。

  4. 在使用Java中的List.toArray方法将List转为数组时,我们需要指定转换后的数组类型。这个方法可以将一个List对象转换为其对应的数组形式。

List<String> list = Lists.newArrayList("1", "2", "3");
String[] array = (String[]) list.toArray();
System.out.println(array);

此时强转为 String[] 会出现异常。

99%的人都踩过坑,全面整理60例Java避坑指南! 需要指定类型,将其转换为具体的数组,因为List在运行时会进行类型擦除,所以需要重新指定入参的类型。可以使用list.toArray(new String[0])进行转换。在这种情况下,不需要给String数组指定长度,指定0即可,toArray方法会自动处理。

    List<String> list = Lists.newArrayList("1", "2", "3");
    String[] array = list.toArray(new String[0]);
    System.out.println(array);
  1. ArrayList是一个非线程安全的集合类,可以使用Collections.synchronizedList方法将其转换为线程安全的List。但是,由于get方法也使用synchronize关键字修饰,这会严重影响并发读取。可以考虑使用CopyOnWriteArrayList来代替。

  2. 使用ArrayList.subList方法并不会修改原始对象,需要通过返回值获取subList。

  3. 在使用Optional.of方法时,如果参数为null,会抛出异常。如果允许参数为null,请使用Optional.ofNullable方法。

  4. SimpleDateFormat是一个非线程安全的类,可以使用线程局部变量避免多线程访问的问题。或者可以使用JDK 8中引入的线程安全的Formatter类DateTimeFormatter来代替,例如DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("mm:ss")

  5. 慎用 Files.readAllBytes 方法一次性读取磁盘文件,这可能导致内存溢出的问题。建议使用 File 类和 BufferedReader 类的缓冲区循环读取文件,以避免一次性加载过多数据。

  6. 在比较大小时,可以使用 Comparable 接口。该接口定义了 compareTo 方法,当当前对象大于目标对象时,返回正数,即按升序排序;当当前对象小于目标对象时,返回负数,即按降序排序。

  7. 在使用 Class.isAssignableFrom 方法时,如果子类在后面,父类在前面,则返回 true;否则返回 false。

if (Object.class.isAssignableFrom(String.class)) {
    System.out.println("true");
}
  1. Redis 客户端 Jedis 不具备线程安全性,因此需要通过使用 JedisPool 来进行管理。

  2. 对于使用 Spring Async、Transactional 等注解的方法,需要注意它们应该被调用于不同的类中。如果在本类中调用这些注解,其效果将无法生效。

  3. 涉及到IO请求的连接一定能够要及时关闭。

  4. Java 泛型的实际类型在编译时会被擦除,因此无法在运行时获取其类型参数的具体类型。在运行时,泛型类型会统一被擦除为Object类型。(如果类型参数被定义为 T extends Fruit,则默认为Fruit类型)。泛型的好处是在编译的时候检查类型安全

List<String> list = new ArrayList<String>();  
System.out.println(list instanceof List<String>)

以上代码无法编译通过,JVM中并不存在List<String>.class或是List<Integer>.class,而只有List.class。