JAVA开发中常见的问题

tech2022-08-10  160

Java开发中比较常遇到的问题

BeanUtils的使用日期格式化的时候必须使用yyyy不能使用YYYY使用三目运算符的时候需要主要自动装箱和拆箱导致的空指针问题尽量为Map设置初始容量禁止使用Executors创建线程池ArrayList的subList的结果不能强制转换为ArrayList类型for循环中不要使用“+”进行字符串的拼接禁止在foreach循环里进行元素的remove/add操作禁止直接使用日志框架(如Log4j和Logback等)提供的API布尔类型的字段禁止使用is开头JAVA开发手册嵩山版总结

BeanUtils的使用

尽量不使用Apache 提供的BeanUtils进行属性复制,因为它的性能比较差(该工具类太追求完美,因此做了比较多的校验,兼容以及日志打印等操作,过度的包装导致性能严重下降),推荐使用Spring提供的BeanUtils

日期格式化的时候必须使用yyyy不能使用YYYY

YYYY代表Week Year(大概是指通过某个具体的日期,判断当前所属的年份即当天所在周所属的年份)

SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); SimpleDateFormat sdf1 = new SimpleDateFormat("YYYY"); System.out.println(sdf1.format(sdf2.parse("2019-12-30 12:20:20"))); //以上代码输出2020,当对sdf1进行格式化的时候如果使用的是yyyy则打印2019

使用三目运算符的时候需要主要自动装箱和拆箱导致的空指针问题

对于一个创建的List或者Map,直接对他进行判断空的操作,得到的结果为非空

Map<String,Boolean> map = new HashMap<String, Boolean>(); List<Integer> list = new ArrayList<>(); System.out.println(map != null); //true System.out.println(list != null); //true

使用三目运算符的时候需要主要自动装箱和拆箱导致的空指针问题

例如代码:

Integer a = 1; Integer b = 2; Integer c = null; Boolean flag1 = false; Integer result = flag1 ? a*b : c; //报错空指针,原因获取c的值的时候会进行自动拆箱导致了空指针 --》null.valueOf()拆箱成包装类 Map<String,Boolean> map = new HashMap<String, Boolean>(); Boolean f = (map!=null ? map.get("test") : false); //反编译的代码如下: HashMap hashmap = new HashMap(); Boolean boolean1 = Boolean.valueOf(hashmap == null ? false : ((Boolean) hashmap.get("test")).booleanValue()); hashmap.get(“test”)->null; (Boolean)null->null; null.booleanValue()-> 报错

对于三目运算符而言:当第二或者第三位操作数为基本类型和对象的时候,其中的对象就会自动拆箱为基本数据类型,此处需要注意避免NPE.

尽量为Map设置初始容量

HashMap类中的一些常量:

//默认初始容量(16) static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; //装载因子,在map的大小达到容量的多少时进行自动扩容(涉及到时间和空间的权衡) static final float DEFAULT_LOAD_FACTOR = 0.75f; //树化阈值 static final int TREEIFY_THRESHOLD = 8; //树退化阈值 static final int UNTREEIFY_THRESHOLD = 6;

容量capacity表示当前可以装的元素数量,size表示当前已经装了的元素数量。

JAVA开发手册建议在初始化HashMap的时候,应该尽量指定其大小,尤其是在你已知map中存放的元素的个数的时候。 默认情况下HashMap的容量为16,但是如果客户通过构造函数指定了一个数字作为容量,那么Hash会选择大于该数字的第一个2的幂作为容量。

当我们没有指定hashMap的初始容量的时候,随着元素的不断增加,HashMap需要进行多次扩容,而每次的扩容机制决定了每次扩容都需要重建hash表,是非常的影响性能的。

HashMap的初始容量建议设置为 期望容量/0.75F + 1.0F (在没有重新设置装载因子的时候),当HashMap达到了阈值的时候会自动进行扩容。

禁止使用Executors创建线程池

禁止使用Executors创建线程池(避免耗费过量的资源导致内存溢出),Java开发手册中明确指出线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,避免资源耗尽的风险。

推荐使用的线程池创建方式(通过ThreadPoolExcutor的构造函数来创建线程池,并且在创建的同时给BlockQueue指定容量),创建代码例子如下:

private static ExecutorService executor = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS,

new ArrayBlockingQueue(10)); 当提交的线程数量超过可用线程的时候,就会抛出 java.util. concurrent.RejectedExecutionException 异常

另外也推荐使用Guava提供的ThreadFactoryBuilder来创建线程池,它可以自定义线程的名字:如

//第六个参数表示用什么创建一个新的线程 ExecutorService executor = new ThreadPoolExecutor(8, 8, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(10), new ThreadFactoryBuilder().setNameFormat("Visitor_Analysis_FutureTask-%d").setDaemon(true).build(), new ThreadPoolExecutor.AbortPolicy());

ArrayList的subList的结果不能强制转换为ArrayList类型

ArrayList的subList的结果不能强制转换为ArrayList类型,否则会抛出ClassCastException异常,subList获取到的是一个List的视图(使用的是List的内部类SubList,和ArrayList之间没有继承关系不能强转),在操作视图的时候将所做的操作同步到原来的List中,并且在这里如果去改变原来的List然后在去获取或者操作subList的话会抛出java.util.ConcurrentModificationException异常

另外Java开发手册中还规定在使用subList的场景中,高度注意对原集合元素的增加或者删除,均会导致子列表的便利,增加,删除产生ConcurrentModificationException

for循环中不要使用“+”进行字符串的拼接

不建议在for循环中使用“+”进行字符串的拼接,这里的+其实是计算机程序中的一种**===**(指定计算机语言中添加某种语法,这种语法对语言的功能没有影响,但是更方便程序员的使用,语法糖让程序更加简单,有更高的可读性)

字符串拼接的几种方式:

StringBuffer (线程安全,可变)使用append方法进行拼接StringBuilder(线程不安全,可变)使用append方法进行拼接StringUtils.join (性能比较差)”+“: 本质上是通过创建StringBuilder对象然后进行拼接Concat: String wechat = "Hollis"; String introduce = "每日更新Java相关技术文章"; String hollis = wechat.concat(",").concat(introduce);`

在循环体中如果循环次数较多的话需要进行字符串拼接不要失使用“+”的方式,因为这样的话每次都需要new一个StringBuilder然后将String转换为StringBuilder,在进行append,而频繁的创建新的对象会耗费很多的时间并且会耗费比较多的内存资源。

效率比较:StringBuilder<StringBuffer<concat<+<StringUtils.join (时间从短到长),但是这里使用****的时候需要注意线程安全问题

禁止在foreach循环里进行元素的remove/add操作

禁止在foreach循环里进行元素的remove/add操作,remove元素需要使用Iterator方式,如果为并发操作,需要对Iterator进行加锁。

*使用普通for循环的时候不会抛出异常

// 使用双括弧语法(double-brace syntax)建立并初始化一个

List List<String> userNames = new ArrayList<String>() {{ add("Hollis"); add("hollis"); add("HollisChuang"); add("H"); }}; for (int i = 0; i < use=====rNames.size(); i++) { if (userNames.get(i).equals("Hollis")) { userNames.remove(i); } } System.out.println(userNames);

*foreach循环又被称为增强for循环,增强for循环中使用了While 和 Iterator实现,在增强for循环中删除或者添加元素都会导致异常 —>触发了java集合的错误检测机制-fail-fast(是java集合的一种错误检测机制,认为有多个线程对非fail-safe的集合进行结构上的改变)

final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }

这里会去判断modCount和expecteModCount的值是否相等,判断为不想等的时候直接抛出了ConcurrentModificationException异常,其中modCount是ArrayList中的一个成员变量,它表示该集合的实际被修改的次数。expectedModCount是ArrayList中的一个内部类Itr中的一个成员变量,表示这个迭代器期望该集合被修改的次数。

在使用集合对象remove()方法删除对象的时候,只会让modCount的值自增,而不操作expectedModCount,导致遍历下一个元素的时候modCount的值和expectedModCount的值不相等,而直接抛出异常。

*需要添加或者删除元素的正确做法:

直接使用普通for循环(for循环没有用到Iterator的遍历,不会触犯fail-fast校验)

直接使用Iterator提供的remove()方法,Iterator提供的remove()方法会修改到expectedModCount的值。

使用java8提供的filter进行过滤。

在使用增强for循环进行元素的增加或者删除完之后使用break跳出循环,使得该方法不会进行下一个元素的判断(使用Iterator.next),但这种方法一次只能进行一次增加或者删除的操作

禁止直接使用日志框架(如Log4j和Logback等)提供的API

禁止直接使用日志框架(如Log4j和Logback等)提供的API,而是使用日志门面的API。这样的话开发不用关心日志框架的实现以及细节,也能够减少更换日志框架之后所带来的成本,也就是说在更换了日志框架之后基本不会影响原来工程中的代码,这也是日志门面的一个重要的好处:解耦。

布尔类型的字段禁止使用is开头

对于布尔类型的字段禁止使用is开头,否则部分框架解析会引起序列化错误(定义为基本数据类型boolean isSuccess的属性,获取该属性的值的方法是isSuccess(),RPC框架在反向解析的时候,会认为该属性名称为success,导致属性获取不到,进而抛出异常)

*Boolean对象的默认值为null,而boolean对象的默认值为false

*对于基本数据类型自动生成的setter,getter方法名称为setXXX()和isXXX(),在序列化和反序列化的时候kennel会导致错误

*关于基本数据类型和包装数据类型的使用标准:

1.所有的POJO类属性必须使用包装数据类型(注意空指针的发生)

2.RPC方法的返回值和参数必须使用包装数据类型

3.所有的局部变量推荐使用基本数据类型

JAVA开发手册嵩山版总结

公司同事分享的一个文档,虽然以前也有读过JAVA开发手册,但是看了下目录发现书中的大多问题自己以前也确实遇到过或者思考过,于是利用早晚搭地铁的时候过了一遍,现在做了一个小总结,给自己加深下印象,也方便日后可以更好的去回归。

最新回复(0)