1. 聚能聊>
  2. 话题详情

Java开发者们,那些年我们一起踩过的坑

lpajwtbt_2
  
阿里大法 《阿里巴巴Java开发手册(正式版)》发布! 当然好的规范在一定程度上可以避免一些坑。

然而在实际开发中,由于开发者水平不同,写出的代码质量也有所迥异。

我们每天都在写Java程序,可能我们更多的是为了结果,但是在写代码的过程中还是有许多需要注意的地方。
  
一些初级开发者甚至老手为了方便可能会在Spring中的Service中添加成员变量,由于Spring的bean默认是单例模式,对于单例模式来说,不仅方便多线程调用该实例,更主要是减小了频繁创建带来的系统消耗。然而在多线程下并发会导致数据混乱。
  
最容易被忽视的Integer数据类型比较,使用==比较 而得到与期望相反的结果。

Integer a = 128;
Integer b = 128;
System.out.println(a==b);

  

HashMap size陷阱,本意是希望给HashMap设置初始值, 避免扩容(resize)的开销,但没有考虑当添加的元素数量达到HashMap容量的75%时将出现resize。

Map map = new HashMap(collection.size());  
for (Object o : collection) {  
      map.put(o.key, o.value);  
}

  
不使用finally块释放资源,导致一直占用内存。

多年的开发后,多多少少大家都会有经历过各式各样的暗坑,只有这样才会促使开发者成长。大家又有踩过那些"暗"坑呢?筒子们都来聊聊,彼此长长经验。

参与话题

奖品区域 活动规则 已 结束

  • 奖品一

    淘公仔 x 3

  • 奖品二

    聆听专属T恤衫 x 3

  • 奖品三

    淘公仔U盘 x 2

66个回答

0

孤尽

《阿里巴巴Java手册》中,以实际案例出发,以Java开发者为中心视角,来决定是否留用此条规则。Java语言本身并没有太多这样那样的漏洞,一切是与场景有关,与错误的理解有关,阿里是一家大公司,有着近万人的开发人员,难以保证每个人对于知识点的认识与运用都是一样的,我们精心筛选了很多案例,总结成册,希望帮助到大家;至于为什么要有数据库、安全、日志,这是与Java工程师悉悉相关的知识点,比如,数据结构的考量不一样,那么上层代码的实现就非常痛苦。最简单地说,对于是与否,公司里有三套做法:1/0,Y/N,true/false,这样导致上层应用经常踩坑,沟通成本增加。本手册已经更新至1.0.2版本,修正了第一版的近15个问题,我们以虚心的态度,开放的心态与大家一起快乐工作,轻松生活。

聚小编 回复

来帮你上个链接,《阿里巴巴JAVA手册》下载入口https://yq.aliyun.com/articles/69327

评论
6

抠脚大叔 已获得淘公仔 复制链接去分享

我也来说几个吧,刚出来工作的时候,最常见的就是str.equals("")这种容易空指针的语句了,虽然一直提醒自己别用,但是还是会不经意间用上。
还有就是使用float,double进行计算的时候,直接使用+,-这些运算符会出现精度问题,需要使用BigDecimal类运算。
在对list进行遍历的时候,如果使用下标遍历,需要在循环中移除元素的时候,不要忘记对下标减1。
还有新手容易犯的问题,部署项目的时候,项目上传的文件不要放在部署目录下,因为这样下次需要升级进行部署的时候,上传的文件就丢了,最好使用nginx进行静态转发,或者tomcat中配置一个虚拟路径。
JAVA引用类型作为参数传递时,如果对形参进行new操作,实参是不会改变的。
在jsp中使用注释,注释中的Java代码还是会编译的喔~
暂时就先想到了这么多,有点杂,想到其他的再补充吧😬

小柒2012 回复

哈哈 不愧是 抠脚大叔

抠脚大叔 回复

哈哈,其实我还很年轻,95哒~

3dp5.net 回复

我36岁了,一点基础都没有,我没有学历,只读了初中二年,对这些好感兴趣,我还能学这些吗?😒😨😢

菜鸡慌慌哒 回复
回复@3dp5.net:

你63也不影响学习啊,只是如果再想以这个就业, 就有点费尽了

前戏 回复

文件传到项目目录下这个坑经历过,后来也是通过tomcat虚拟目录解决了

绝尘 回复

哈哈

评论
4

我很无奈 已获得淘公仔 复制链接去分享

ReentrantLock 锁大家估计都会有接触,之前做公益直捐的时候,遇到个问题,由于汇付的回调是有同步异步之分,回调接口会被调用俩次,而我们在回调接口中有更新公益项目以及增加捐赠记录,生成合同等操作,这样的话 就会导致重复的数据,所以刚开始的时候 在回调接口用到redis做了个key,以此key来判断是否已经被调用过,但后来发现,同步和异步有时候是在几乎相差一毫秒的时间去调用,这样的话 redis的key还未设置好,第二次请求就来到,导致重复数据产生,为了解决这个问题,就想到了锁,但由于本人对ReentrantLock 不是很熟悉,所以把锁 在接口内 进行了new ,所以每次来 都会new一次锁,根本未能起到锁的作用,最后private final Lock lock = new ReentrantLock(); 以 成员变量来定义,但一定要在最后释放锁,QQ_20170222102803仅以我们项目为例,只是个思路,尚有不足,多提意见,多谢!

sunny.don 回复

为什么不用redis的锁和事务,分布式环境下这个lock应该有问题吧

我很无奈 回复

业务场景不同

绝尘 回复

redis的锁?

评论
2

军卫 已获得聆听专属T恤衫 复制链接去分享

16年底快放假了
1.memcache经常报超时,各种分析memcache,网络连接都没问题。超时时间从2秒改成2.5秒,连接池由一个改成了4个。发上去没问题,过一天多又开始报异常。都TM快放假了,被这搞得牙疼。然后各查,发现从重启服务1.5天左右后开始FullGC特别频繁,而且时间达到1.5秒左右,有时更长。+UseParallelOldGC。霍然开朗啊……
2.线上dump出来内存。发现是PreparedStatement特别多,见鬼了,只是前一段时间调大了连接池的连接池,这个东西咋会多,以前也没问题啊,于是各种查配置,无果,只能去debug dbcp源码了,居然发现Preparedsatement是从连接池里生成了,以前没看过,只知道Connection有池了,没想到Preparedsatement也有,然后组内的小伙伴们帮忙开始查,各种搜然后:http://commons.apache.org/proper/commons-dbcp/configuration.html
poolPreparedStatements false Enable prepared statement pooling for this pool.
maxOpenPreparedStatements unlimited The maximum number of open statements that can be allocated from the statement pool at the same time, or negative for no limit.

NOTE - Make sure your connection has some resources left for the other statements. Pooling PreparedStatements may keep their cursors open in the database, causing a connection to run out of cursors, especially if maxOpenPreparedStatements is left at the default (unlimited) and an application opens a large number of different PreparedStatements per connection. To avoid this problem, maxOpenPreparedStatements should be set to a value less than the maximum number of cursors that can be open on a Connection.

某位前辈只配置poolPreparedStatements =true,没有配置maxOpenPreparedStatements。而且我们操作DB多数是insert into xxx values list。list一般是1到3000左右的。
最后直接去掉这了poolPreparedStatements 的配置,上线了,世界安静了……

小柒2012 回复

放假前的煎熬嘛~~

评论
3

it民工198807 复制链接去分享

hashMap突然让我想到 之前开发遇到两天崩一次的问题,开始以为spring单列多列的问题,现在想想或许有些解决方案。可惜的是我辞职了……

小柒2012 回复

来来 来 说说 两天崩一次的心情

聚小编 回复

排队等听细节

屁孩儿 回复

坐等details

水帘听雨 回复

排队等听细节

打大灰机 回复

我猜是你事物是手动控制,但是没有合理的关闭事物

评论
1

scruel 已获得聆听专属T恤衫 复制链接去分享

说几个目前作为新手遇到的几个常见坑吧:
1.double类型使用==比较,一般应该是以JUnit中给定精度的比较为好。(String就不说了)。
2.子类在调用无参构造方法创建对象时,忽略了父类的无参构造此时也会被调用。
3.在map的迭代中使用map.remove()。
4.static变量的概念混淆不清,导致生成多个对象后变量值混乱。
5.代码格式、命名规范不符合业界规范。
6.忘记对资源关闭,多线程的时候时常出现无法访问情况。
7.同名类用错包,刚遇到这种情况的时候查半天。
8.字节流字符流概念混淆不清,导致写入、读入文件时发生编码混乱情况。
9.过分依赖正则匹配,导致程序进入正则死循环。(之前看到这个BUG已经被Oracle标记为不会修复。。)
10.最坑的莫过于,没有写注释习惯的几天后。。
(11.撸码无法自拔到没有女朋友或女友离去。)

小柒2012 回复

虽然 你才大二,但是遇到也不少了~~

scruel 回复

嗯,云栖是个好地方,默默偷学中。。。谢谢礼物~~

评论
3

海岸无眠 已获得淘公仔 复制链接去分享

这里说下工作中遇到的几个坑:

一、都知道Spring的bean默认是单例模式,由于要为了方便使用成员变量(历史遗留问题),所以改成了prototype 也就是非单利模式。然而后面的事情是居然又在里面开启了多线程,这时候共有的是一个bean,所以导致了混乱。

二、楼主也提出了 Intger比较的问题,有一次是对Short 类型比较使用了== 怎么也到不到想要的记过,日志输出了一下 果然是false。

三、关于HashMap由于是线程不安全的, 在多线程的 不经意使用,导致高并发下 拖死机器的情况。

四、还有就是 单例模式不加锁 导致初始化的时候生成多个实例,种种影响开销的问题。

五、struts2 中属性只写了set方法 没有写get方法 导致 下一个action中 无法获取到参数。

六、JAVA中split方法分割字符串,关于特殊符号转义的问题,永远得不到想要的数据。

还就就是大家常见的空指针问题,一定要比较放在后面~~~~~

2

屁孩儿 已获得淘公仔U盘 复制链接去分享

那些年曾经趟过的小水洼~~
1.字符串连接
误:
String s = "";
for (Person p : persons) {

s += ", " + p.getName();  

}
s = s.substring(2);
正:
StringBuilder sb = new StringBuilder(persons.size() * 16); // well estimated buffer
for (Person p : persons) {

if (sb.length() > 0) sb.append(", ");
sb.append(p.getName);

}
2.数字转换成字符串
误:
"" + set.size()
new Integer(set.size()).toString()
正:
String.valueOf(set.size())
3.不必要的初始化
误:
public class B {
private int count = 0;
private String name = null;
private boolean important = false;
}
正:
public class B {
private int count;
private String name;
private boolean important;
}
4.还有jdbc链接mysql不指定编码类型等等。“==”不好随便用,Integer是其中一个2866b71fbe096b6306d785340e338744eaf8acad

聚小编 回复

水洼还真不少

评论
1

李四爷 已获得淘公仔U盘 复制链接去分享

当时学习JAVA时的写的笔记,看到一些现在已经遗忘的细节。稍微整理了几个,发出来与大家分享。代码很简单,不运行你能得出正确结果吗
运行结果Ctrl+A就能看到

一、动态绑定:
public class Test {

public static void main(String[] args) {

    A obj = new B();

    int n = 2;

    obj.method(n);

}

}

class A {

public void method(double n) {

    System.out.println("A:double");

}

}

class B extends A {

public void method(int n) {

    System.out.println("B:int");

}

}

运行结果:A:double

二、switch语句
public class Test {

public static void main(String[] args) {

    int value = 2;

    switch (value) {

    case 1:

        System.out.print("a");

        break;

    case 2:

        System.out.print("b");

    default:

        System.out.print("c");

    case 3:

        System.out.print("d");

        break;

    }

}

}

运行结果:bcd

三、泛型可变参数
public class Test {

public static void main(String[] args) {

    method(new int[]{1, 2, 3});

    method(1, 2, 3);

    method(1, 2.1);

}

    

public static <T> void method(T... s) {

    System.out.println(s.getClass());

}

}

运行结果:

class [[I
class [Ljava.lang.Integer;
class [Ljava.lang.Number;

当然,java语言细节方面的陷阱很多,大家知道的都可以分享一下

scruel 回复

对于第一个动态绑定,可否这样解释:“声明的是父类的引用,程序首先列出了该声明类中的method方法,匹配到了符合条件的method方法(int类型可以被转换为double类型),然后就执行该方法。”

李四爷 回复

在哪设置地址 邮寄礼品啊

评论
1

peaceful 已获得聆听专属T恤衫 复制链接去分享

记得有一次项目上线,测试要做压测,然后压就压呗。然而就几十个并发,数据一不对。当时左思右考,一直还以为是程序本身问题。最终查了下,原来是程序中JAVA配置的最大数据连接为5的问题,不出错才怪了。

以上问题也在Dubbo上线测试的时候遇到过threads默认配置成了10,然后就没然后了。

还有一次就是,Linux和Windows下文件分隔符的问题,导致项目上线后获取不到指定文件,打印出来果然不对,记得要使用 System.getProperty("file.separator");

1

李四爷 复制链接去分享

Java中创建对象的常规方式有如下5种:

  1. 通过new调用构造器创建Java对象;
  2. 通过Class对象的newInstance()方法调用构造器创建对象;
  3. 通过Java的反序列化机制从IO流中恢复对象;
  4. 通过Java对象提供的clone方法复制一个对象;
  5. 基本类型及String类型,可以直接赋予直接量。
    对于Java中的字符串直接量,JVM会使用一个字符串池来保存它们:当第一次使用某个直接量时,JVM会将它放入字符串池中缓存,在一般情况下,字符串池中的字符串不会被Java回收器回收,当程序再次使用该直接量时,无需重新创建一个新的字符串,而是直接让引用变量指向字符串池中已经存在的字符串。

字符串池中的字符串是不会被回收的,这是Java内存泄漏的一个原因。
如果程序需要一个字符序列会改变的字符串,那么应该考虑使用StringBuilder或StringBuffer,当然,最好还是使用StringBuilder,因为对于线程安全的StringBuffer,StringBuilder是线程不安全的,也就是说,StringBuffer中的绝大部分代码都加了synchronized修饰符。
如果你的代码所在的程序或进程是多个线程同时运行的,而这些线程会同时运行这段代码,如果每次运行结果和单线程运行结果一样,而且其他变量的值也和预期的一样,就是线程安全的,或者说,多个线程的切换不会导致该接口的执行结果存在二义性,我们就说该接口是线程安全的。
Java是强类型语言,所谓强类型语言,通常具有两个基本特征:

  1. 所有变更必须先声明才能使用,声明变量时必须指定该变量的数据类型;
  2. 一量某个变量的数据类型确定下来,那么该变量永远只能接受该类型的数据,不能接受其他类型的数据。
    当一个算术表达式中包含多个基本类型时,整个算术表达式的数据类型会自动提升,这是我们已经知道的规定。在此之外有个特例,请看以下代码:

[java]
short sv = 5;
sv = sv - 2;

我们通常不能理解的一个问题是:这段代码会报错。但结合数据类型的自动提升,我们可以这样理解:sv - 2中,2是int型,所以sv - 2的结果是一个int型,所以不能赋值给sv。
再看以下代码:
[java]
short sv = 5;
sv -= 2;

如果你再用自动类型提升来理解的话,你会解释为这一段代码结果也会报错,可惜,Java中还有另一规定:复合赋值运算符包含了一个隐式的类型转换,sv -= 2其实等价于sv = (short)(sv - 2)。
显然这里又出现了另外一个问题:将巨大的int转化为short会出什么问题吗?且看如下代码:
[java]
short sv = 5;
sv += 9000;

我们已知,short类型的数值范围在-32768~32767之间,所以当把9005赋值给sv时,就会出现高位截断,sv的最终结果为24471。
由此可见,复合赋值运算符简单、方便,而且具有性能上的优势,但复合赋值运算符可能有一定的危险:它潜在的隐式类型转换可能会不知不觉中导致计算结果的高位截断。
且看以下代码:
[java]
List list = new ArrayList();
list.add("a");
list.add("b");
list.add("c");
List intList = list;
for (int i = 0; i < list.size(); i++) {
System.out.println(intList.get(i));
}

当我看到这段代码的时候,我理所当然的认为这段代码是错误的,因为list里面包含的是String,不能赋值给List,而在Eclipse里面运行这段代码后,你会发现这段代码没有任何错误,可以编译,也可以运行,这就是泛型里面的陷阱。在使用泛型时,要注意以下几点:

  1. 当程序把一个原始类型的变量赋值给一个带泛型信息的变量时,总是可以通过编译,只是会提出一些警告,如上述代码中,List intList = list并不会报错;
  2. 当程序试图访问带泛型声明的集合的集合元素时,编译器总是把集合元素当成泛型类型处理——它并不关心集合里集合元素的实际类型,如上述代码中,intList.get(i)的结果是一个Integer类型;
  3. 当程序试图访问带泛型声明的集合的集合元素时,JVM会遍历每个集合元素自动执行强制类型转换,如果集合元素的实际类型与集合所带的泛型信息不匹配,运行时将引发ClassCastException异常,假设在上述代码的for循环中加上Integer in = intList.get(i),就会报此异常。
    上面讲的是把原始类型赋值给泛型类型,假如反过来呢?且看以下代码:

[java]
List list = new ArrayList();
list.add("a");
list.add("b");
list.add("c");
List li = list;
for (int i = 0; i < list.size(); i++) {
System.out.println(li.get(i));
System.out.println(li.get(i).length()); // 1)
}
你会发现1)处的代码编译错误,这是因为——把一个带泛型类型的Java变量赋值给一个不带泛型类型的变量时,Java程序会发生擦除,这种擦除不仅仅会擦除实际类型实参,还会擦除所有泛型信息,如上述代码,li.get(i)是终被当成一个Object对象使用。
Java泛型设计原则:如果一段代码在编译时系统没有产生“[unchecked]未经检查的转换”警告,则程序在运行时不会引发ClassCastException异常。
从JDK1.5开始,Java提供了三种方式来创建、启动多线程:

  1. 继承Thread类来创建线程类,重写run()方法作为线程执行体;
  2. 实现Runnable接口来创建线程类,重写run()方法作为线程执行体;
  3. 实现Callable接口来创建线程类,重写run()方法作为线程执行体;
    其中,第一种方式效果最差,它有两点坏处:
  4. 线程类继承了Thread类,无法再继承其他父类;
  5. 因为每条线程都是一个Thread子类的实例,因此多个线程之间共享数据比较麻烦。
    对于第二种和第三种,它们的本质是一样的,只是Callable接口里面包含的call()方法既可以声明抛出异常,也可以拥有返回值。

无论使用哪种方式,都不要调用run()方法来启动线程:启动线程应该使用start()方法,如果程序从未调用线程对象的start()方法来启动它,那么这个线程对象将一直处于新建状态。

小柒2012 回复

用自己的话 组织一下

聚小编 回复

wo~wo~~坑好深

gengli 回复

各种坑啊!

评论
1

00海绵宝宝 复制链接去分享

(1)关于JAVA中equest和hashcode的坑。拿set来举例,set中的元素和map中的key不能重复,是指两个对象的equals返回true则代表重复,其实其本质是看两个对象的哈希值是否相等,如果两个对象的equals返回true,但他们的哈希值不等时也允许插入;理论上Java对象的哈希值不等则他们的equals也会返回false,但是Java可以重写对象的equals方法;所以究其本质其实是判断两个对象的哈希值是否相等;另外一点,一个对象的哈希值是通过调用该对象的hashCode()方法,将该对象的内部地址转换成一个整数的返回值(考虑基本数据类型和引用数据类型),因此一个对象的哈希值并不是一直相等的、固定的,而是经常变化的;

(2)容器不能持有基本类型,且容器的泛型只作用于编译阶段,一旦脱离编译阶段,则泛型对容器无效。比如通过反射,泛型对容器是无效的QQ_20170227172057
用反射进行put,绕过了泛型检查;get时泛型会自动进行类型转换,从而导致了ClassCastException

(3)对于字节流和字符流:字节流(InputStream/OutputStream)是直接操作文件本身的,即使不关闭输出流文件也会写入;字符流(Reader/Write)操作时使用了缓冲区,只有当关闭输出流或者flush的时候才会将内容从缓冲区输出

00海绵宝宝 回复

喜欢那个纪念T恤.....@楼主

hzaaaa 回复

那里

评论
0

云天白蓝 复制链接去分享

我感觉 JAVA开发中遇到最大的坑就是产品经理。

小柒2012 回复

哈哈哈 绝对是个大坑!!!

打大灰机 回复

有什么好怨的,说的就好像你写代码没有bug似的~记住,谁都可能犯错

评论
1

章小凡 复制链接去分享

印象最深的一次当时做个excel导入,导入后解析数据,各种逻辑,然后入库。当时起多线程去分批处理excel的内容,当时没考虑到线程数的问题,导致每次导入都因为线程太多崩了(坑1),后来在执行逻辑的时候,一直NullPlonterException,后来咨询了老司机才知道(坑2)web容器在启动应用时,并没有提前将线程中的bean注入(在线程启动前,web容器也是无法感知的),一把辛酸泪。。。。。

小柒2012 回复

多核CPU开几个线程最好,其实并不是越多越好,多了线程之间切换可能导致崩,一般来说 服务器端最佳线程数量=((线程等待时间+线程cpu时间)/线程cpu时间) cpu数量,简便公式是CPU数量 2 + 2。

线程中 过去bean的话 貌似要手动get的。

评论
1

wayne87 复制链接去分享

私有内部类记得尽量用static呦

hzaaaa 回复

评论
0

李四爷 复制链接去分享

小编记得U盘给我哦 ,以后学习可以存放资料,谢谢啦!

小柒2012 回复

记得好好学习 天天向上

李四爷 回复

谢谢 ,u盘还没收到哦

评论
0

舍恩 复制链接去分享

算了,只想领个优盘

小柒2012 回复

赶紧说说你遇到的坑。。。

评论
0

舍恩 复制链接去分享

在学校还没学习java😭😭😭

小柒2012 回复

那你可以想象一下下。

评论
1

szm. 复制链接去分享

当使用了很多的
try{
try{

}catch(){

}
}catch(){

}finally{

}
并且在使用中还进行嵌套操作,那么很容易出现变量某些变量没有关闭,导致一直占用资源。并且有些人喜欢提示try catch的时候就surround with一个,导致程序的可读性变差。
因为java的ide众多,也导致了代码编写时的编码千变万化,很多人将别人的代码导入修改时不把编码该还就直接保存,导致后来的人无论调成什么编码,中文注释部分都是一团乱码,因此也呼吁使用英文编码,减少编码问题的影响。
说到中英文问题,不得不说符号的问题,什么中文引号,括号,冒号和分号都弱爆了,全角空格才是真的坑,毕竟它就是一个空白占位符,但是编译会报错,有经验的很容易找到,但是新手么,很容易中招的。
最后,阿里巴巴java开发手册真的很有意义,按照上面的要求去做,能减少很多因为代码风格等问题导致的时间浪费。

9随遇而安 回复

是catch 不是cache 😂

szm. 回复

感谢指正,讲真,这个单词很少手动敲出来,一般都是IDE生成

评论
0

1389784009687553 复制链接去分享

到处都是坑,认真点,细心点

小柒2012 回复

经验 也是一部分

评论
0

1538087732401929 复制链接去分享

我希望大家可以用爱心……用善良……去关注自己的慈善事业……

小柒2012 回复

你是说 大家一起维护 阿里巴巴Java手册 嘛~~~~

评论
4