• 那些年我踩过的坑,你踩中了几个


    背景

    从事软件行业已经多年了,在生产和开发中也踩过了相关坑,今天分享给大家,希望大家能够积累相关经验,避免重复踩坑。

    Integer自动装箱和拆箱

    举个例子

    public static void main(String[] args) 
        {
            Integer x1 = 10;
            Integer x2 = 10;
            boolean r = x1==x2;
            System.out.println("x1==x2:" + r);
    
            Integer y1 = 300;
            Integer y2 = 300;
            boolean r1 = y1==y2;
            System.out.println("y1==y2:" + r1);
    
            Integer m1 = new Integer(300);
            Integer m2 = new Integer(300);
            boolean r2 = m1==m2;
            System.out.println("m1==m2:" + r2);
            
            Integer n1 = new Integer(20);
            Integer n2 = new Integer(20);
            boolean r3 = n1.intValue()==n2.intValue();
            System.out.println("n1==n2:" + r3);
       }
    复制代码

    说明:关于Integer拆箱和装箱的问题,只需要掌握Integer默认缓存范围,那么这些问题就很容易得出答案。Integer是int类型的包装类,当int值赋值给Integer时会使用valueOf(int i)方法自动装箱.默认情况下cache[]缓存范围是[-128,127],

    String的==和euals问题

    关于String的==和equal的问题,当年实习面试的时候,第一题问题的就是这个,结果打错了,面试官说出门右转。所以对于这个记忆犹新。

    举个例子

    public static void main(String[] args)
        {
            //
            String s1 = "abc";
            String s2 = "abc";
            System.out.println("s1 == s2 : " + (s1 == s2));
            
            String s3 = new String("abc");
            String s4 = new String("abc");
            System.out.println("s3 == s4 : " + (s3 == s4));
            
            String s5 = "ab" + "cd";
            String s6 = "abcd";
            System.out.println("s5 = s5 : " + (s5 == s6));
            
            String str1 = "ab"; 
            String str2 = "cd";
            String str3 = str1 + str2;
            String str4 = "abcd";
            System.out.println("str4 = str3 : " + (str3 == str4));
            
            String str6 = "b";
            String str7 = "a" + str6;
            String str8 = "ab";
            System.out.println("str7 = str8 : " + (str7 == str8));
            
            //常量话
            final String str9 = "b";
            String str10 = "a" + str9;
            String str11 = "ab";
            System.out.println("str9 = str89 : " + (str10 == str11));
            
            String s12="abc";
            String s13 = new String("abc");
            System.out.println("s12 = s13 : " + (s12 == s13.intern()));
        }
    复制代码

    说明:关于String的==和equal需要掌握String的内存模型,那么上面的问题能够准确的说出答案,不怕踩坑了。

    精度丢失问题

    关于精度丢失的问题主要体现在浮点类型和BigDecimal的数据,例如如下例子

    public class BigDecimalTest
    {
       public static void main(String[] args)
        {
              float a = 1;
              float b = 0.9f;
              System.out.println(a - b);
        }
    }
    复制代码

    输出结果却是:0.100000024,这是因为0.1的二进制表示是无限循环的。由于计算机的资源是有限的,所以是没办法用二进制精确的表示 0.1,只能用「近似值」来表示,就是在有限的精度情况下,最大化接近 0.1 的二进制数,于是就会造成精度缺失的情况。

    所以大家可能会采用BigDecimal类型来进行操作,但是BigDecimal同样也存在精度丢失的问题

    BigDecimal c = new BigDecimal(1.0);
              BigDecimal d =new BigDecimal(3.0);
              BigDecimal e =c.divide(d);
              System.out.println("e = " + e);
    复制代码

    执行程序会报如下错误:

    这是因为如果在除法(divide)运算过程中,如果商是一个无限小数(0.333…),而操作的结果预期是一个精确的数字,那么将会抛出ArithmeticException异常,所以BigDecimal进行相关的乘法和除法时,一定要保留小数位。上述代码修改为如下即可。

    BigDecimal c = new BigDecimal(1.0);
              BigDecimal d =new BigDecimal(3.0);
              BigDecimal e =c.divide(d,2,RoundingMode.HALF_UP);
              System.out.println("e = " + e);
    复制代码

    值传递和引用传递

    实例说明:

    public static void func(int a)
     {
            a=30;
            System.out.println(a);
     }
    public static void main(String[] args) 
    {
      int a=20;//变量
      func(a);
    
      String c="abc";
    
       Strinfunc(c);
    }
    
    public static void Strinfunc(String c)
    {
       c="c";
       System.out.println(c);
    }
    复制代码

    说明:需要理解值传递和引用传递,那么就很容易输出上述例子的答案。

    值传递:是指在调用函数时,将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,就不会影响到实际参数。

    引用传递:是指在调用函数时,将实际参数的地址传递到函数中,那么在函数中对参数进行修改,将会影响到实际参数。

    List.subList内存泄露

    实例说明:

    public static void main(String[] args)
        {
            List cache = new ArrayList();
            try
            {
    
                while (true)
                {
    
                    List list = new ArrayList();
    
                    for (int j = 0; j < 100000; j++)
                    {
                        list.add(j);
                    }
    
                    List sublist = list.subList(0, 1);
                    cache.add(sublist);
                }
            }
            finally
            {
                System.out.println("cache size = " + cache.size());
    
            }
        }
    复制代码

    说明:这是因为SubList的实例中,保存有原有list对象的强引用,只要sublist没有被jvm回收,那么这个原有list对象就不能gc,即使这个list和其包含的对象已经没有其他任何引用。

    for循环中删除元素报错

    示例:

    public static void main(String[] args) {
              String test = "1,2,3,4,5,6,7";
              List testList = Arrays.asList(test.split(","));
              for(int i = 0; i < testList.size(); i++){
                  String temp = testList.get(i);
                      testList.remove(temp);
              }
          }
    复制代码

    运行上述的代码报如下错误:

    Exception in thread "main" java.lang.UnsupportedOperationException
    	at java.util.AbstractList.remove(AbstractList.java:161)
    	at java.util.AbstractList$Itr.remove(AbstractList.java:374)
    	at java.util.AbstractCollection.remove(AbstractCollection.java:293)
    	at com.skywares.fw.juc.integer.ListTest.main(ListTest.java:14)
    
    复制代码

    这是因为fail-fast,即快速失败,它是Java集合的一种错误检测机制。当多个线程对集合(非fail-safe的集合类)进行结构上的改变的操作时,有可能会产生fail-fast机制,这个时候就会抛出ConcurrentModificationException(当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常)。

    解决的办法

    可以通过迭代器来进行删除、或者采用java8的filter过滤 采用java8的filter来过滤

    testList.stream().filter(t -> !t.equals("a")).collect(Collectors.toList());
              System.out.println(testList);
    复制代码

    SimpleDateformat格式化时间

    SimpleDateFormat是大家比较常用的,而且在一般情况下,一个应用中模式都是一样的,所以很多人都喜欢使用如下的方式定义SimpleDateFormat

    public class SimpleDateFormatTest
    {
        private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat(
                "yyyy-MM-dd HH:mm:ss");
    
        public static void main(String[] args)
        {
            simpleDateFormat.setTimeZone(TimeZone.getTimeZone("America/New_York"));
            System.out.println(simpleDateFormat.format(Calendar.getInstance()
                    .getTime()));
        }
    }
    复制代码

    采用这样的定义方式,如果在多线程的情况下,存在很大的安全隐患。

    public class SimpleDateFormatTest
    {
        private SimpleDateFormat simpleDateFormat = new SimpleDateFormat(
                "yyyy-MM-dd HH:mm:ss");
    
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10, 100, 1,
                TimeUnit.MINUTES, new LinkedBlockingQueue<>(1000));
        
        public static void main(String[] args)
        {
            SimpleDateFormatTest simpleDateFormatTest =new SimpleDateFormatTest();
            simpleDateFormatTest.test();
        }
    
        public void test()
        {
            while (true)
            {
                poolExecutor.execute(new Runnable()
                {
                    @Override
                    public void run()
                    {
                        String dateString = simpleDateFormat.format(new Date());
                        try
                        {
                            Date parseDate = simpleDateFormat.parse(dateString);
                            String dateString2 = simpleDateFormat.format(parseDate);
                            System.out.println(dateString.equals(dateString2));
                        }
                        catch (ParseException e)
                        {
                            e.printStackTrace();
                        }
                    }
                });
            }
        }
    }
    复制代码

    运行代码如果出现了false,则说明SimpleDateFormat在多线程不安全,导致结果错误,那么我们如何避免呢?可以采用java8提供的DateTimeFormatter来解决SimpleDateFormat在多线程的不安全问题。

    未释放对应的资源,导致内存溢出

    示例

    ZipFile zipFile = new ZipFile(fileName);
    Enumeration elments = (Enumeration) zipFile.getEntries();
    while (elments.hasMoreElements())
    {
    	System.out.println(elments.nextElement());
    }
    复制代码

    说明:数据库资源,文件资源,Socket资源以及流资源等,这些资源都是有限的,当程序中的代码申请了资源之后,却没有释放,就会导致资源没有被释放,当系统没有充足的资源时,就会导致系统因为资源枯竭而导致服务不可用。

    ThreadLocal内存泄漏

    ThreadLocal也是面试经常被问的问题,但是在使用的过程中需要手动释放对象,否则会报内存泄露问题。

    一个典型的内存泄露例子

    static class UserInfoContext{
        public static ThreadLocal userLocal =new ThreadLocal<>();
    }
    
    @WebFilter("/*")
    public class UserInfoFilter implements Filter{
    
        @Override
        public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
            if(hasLogined(req)){
                UserContext.userLocal.set(extractUserInfo(req));
            }
            chain.doFilter(req, res);
        }
    
    }
    复制代码

    说明:

    当App应用程序被Tomcat装载并运行时,请求在经过UserInfoFilter时,会向执行线程的私有Map中插入一个Entry,这个Entry有一条强引用指向UserInfo对象.而当App被卸载时,虽然Tomcat释放了对servlet/filter对象的引用,但线程的私有ThreadLocalMap仍有引用指向UserInfo对象,而UserInfo对象是reachable,从而导致UserInfo类也是reachable,由于Tomcat做了类隔离, 在下一次加载App时相同的类还会再次加载. 随着App在Tomcat一次次的被加载和卸载,JVMPermGen的类越积越多,最后导致java.lang.OutOfMemoryError: PermGen space.

    总结

    本文列举一些常见的踩坑记录,希望大家能积累经验,避免重复踩坑,如果有疑问,请随时反馈。

     

  • 相关阅读:
    spring 集成redisearch
    STM32时间片轮询实现基于RTC带温湿度的万年历
    智能水厂运行与调控3D模拟仿真在线展示提高整个系统的协同效应
    淘宝商品详情API接口(标题|主图|SKU|价格|商品销量)
    Linux-用户与用户组,权限
    【前端设计模式】之组合模式
    用.NET代码生成JSON Schema 验证器
    lua的模块与类
    分布式锁java程序怎么处理 zk与redis
    使命、愿景、价值观到底有什么区别
  • 原文地址:https://blog.csdn.net/Candyz7/article/details/126566797