Java 基础知识的一些易错点

Posted chy_18883701161

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 基础知识的一些易错点相关的知识,希望对你有一定的参考价值。

 

1、正确使用 equals()

Object的equals方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals。

String str = null;
if (str.equals("abcd")) {
  ...
} else {
  ...
}

如果变量str为null,会抛出空指针异常,如果没有catch来捕获处理(我们一般不会在equals()上加try),程序直接就终止运行了。

 

abcd".equals(str)

把常量写在前面,“abcd”!=null,结果为false,不会抛出异常。

但2个都是变量呢?

 

最推荐下面的方式:使用工具类Objects(JDK7自带的)

Objects.equals(str,"abcd")

就算2个都是变量,2个都是null,都不会抛出异常。如果2个都是null,null==null,返回true。

 

Objects的部分源码如下:

public static boolean equals(Object a, Object b) {
        // 如果a==null的话此时a.equals(b)就不会得到执行,避免出现空指针异常。
        return (a == b) || (a != null && a.equals(b));
}

||、&&都是断路的,如果||前面为true,就不会执行后面的判断;如果&&前面为false,就不会执行后面的判断。

 

equals()的作用范围比==大。

==只能判断相同类型的数据,比如2个都是数值型(数值型归为一类)、都是字符串,都是User类型。如果2个的类型不同,比如 if(1==“1”),一个是数值型、一个是String,通不过编译。

equals()则无此要求,不管2个的数据类型相不相同都可以。

 

 

 

 

2、基本类型、包装类型值的比较

2个都是基本类型,或者2个都是基本类型的常量,

或者基本类型、包装类型(包装类型属于引用类型),或者基本类型、基本类型的常量,或者包装类型、基本类型的常量,

只要2个不全是包装类型,进行比较,不管是使用==、还是equals(),都是使用值进行比较,都可以。

 

如果2个都是包装类型,==比较的是2个对象的地址,肯定不相同。

有特例:如果2个都是Integer,当数值在-128 ~127时,会将创建的 Integer 对象缓存起来,当下次再出现该数值时,直接从缓存中取出对应的Integer对象。

Integer x = 3;  
Integer y = 3;  //2个都是Integer,值相同,且在-128~127之间,不会创建一个新的Integer对象,而是直接指向x指向的对象,即x、y都指向堆中的同一个Integer对象
System.out.println(x == y); // 都是引用类型,==根据地址进行比较,x、y的地址是相同的,返回true

 

包装类型都重写了equals(),都是使用值进行比较,2个都是包装类型应该使用equals()来比较。

 

 

 

 

 

3. BigDecimal的使用

计算机表示浮点数的方式,会造成浮点数精度的丢失,计算机不能精确地表示浮点数:

        float a = 1.0f - 0.9f;
        float b = 0.9f - 0.8f;
        System.out.println(a);// 0.100000024
        System.out.println(b);// 0.099999964
        System.out.println(a==b);// false

不管是单精度、双精度,不管是浮点数的基本类型、还是浮点数的包装类型,都存在这个问题。

当然,双精度能表示的小数位数更多,比单精度更加精确,但小数位数多了之后,依旧会丢失精度。

 

使用BigDecimal类来表示浮点数可解决浮点数精度丢失的问题,因为是以字符串的形式存储数值。

BigDecimal a = new BigDecimal("1.0");  //参数是字符串,传递数值会丢失精度
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);// 0.1
BigDecimal y = b.subtract(c);// 0.1
System.out.println(x.equals(y));// true 

进行数学运算也要使用BigDecimal中提供的方法,确保精度不丢失。

BigDecimal类重写了equals(),是根据值进行比较2个BigDecimal对象。

 

大小比较:

BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
System.out.println(a.compareTo(b));// 1

a.compareTo(b) : 返回 -1 表示前面一个小,0 表示相等, 1表示前面一个大。

 

保留几位小数:

BigDecimal m = new BigDecimal("1.255433");
BigDecimal n = m.setScale(3,BigDecimal.ROUND_HALF_DOWN); //第一个参数指定保留几位小数,第二个参数指定后面部分的处理方式,BigDecimal.ROUND_HALF_DOWN即四舍五入
System.out.println(n);// 1.255

 

BigDecimal的构造函数:

//推荐这种
BigDecimal a = new BigDecimal("1.6");
System.out.println(a); //1.6

//其次是这种,会先执行Double的toString()方法将1.6转换为字符串,再调用上面一种方式创建BigDecimal对象。相比第一种,开销大一些
BigDecimal b = BigDecimal.valueOf(1.6);
System.out.println(b); //1.6

//其它直接传入数值的都不推荐,因为会丢失精度
BigDecimal c=new BigDecimal(1.6);
System.out.println(c); //1.600000000000000088817841970012523233890533447265625

 

第二种的源码如下:

public static BigDecimal valueOf(double val) {
     return new BigDecimal(Double.toString(val));
}

 

BigDecimal 主要用来操作(大)浮点数,BigInteger 主要用来操作大整数(超过 long 型)。

BigDecimal 的实现利用到了 BigInteger, 所不同的是 BigDecimal 加入了小数位的概念

 

 

 

 

4. 基本数据类型与包装数据类型的使用标准

Reference:《阿里巴巴Java开发手册》

  • 【强制】所有的 POJO 类属性必须使用包装数据类型。
  • 【强制】RPC 方法的返回值和参数必须使用包装数据类型。
  • 【推荐】所有的局部变量使用基本数据类型。

比如我们如果自定义了一个Student类,其中有一个属性是成绩score,如果用Integer而不用int定义,一次考试,学生可能没考,值是null,也可能考了,但考了0分,值是0,这两个表达的状态明显不一样.

说明 :POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何 NPE 问题,或者入库检查,都由使用者来保证。

 数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险。

 比如显示成交总额涨跌情况,即正负 x%,x 为基本数据类型,调用的 RPC 服务,调用不成功时,返回的是默认值,页面显示为 0%,这是不合理的,应该显示成中划线。所以包装数据类型的 null 值,能够表示额外的信息,如:远程调用失败,异常退出。

 

 

 

 

 

5. 数组转List

Arrays.asList()这个静态方法可以将数组转换为List:

String[] myArray = { "Apple", "Banana", "Orange" };
List<String> myList = Arrays.asList(myArray);
//也可以直接传入数组元素
List<String> myList = Arrays.asList("Apple","Banana", "Orange");

 

看一下这个方法的源码:

public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

    /**
     * @serial include
     */
    private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
    {
        private static final long serialVersionUID = -2764017481108945198L;
        private final E[] a;

        ArrayList(E[] array) {
            a = Objects.requireNonNull(array);
        }

        @Override
        public int size() {
            return a.length;
        }

        @Override
        public Object[] toArray() {
            return a.clone();
        }

        @Override
        @SuppressWarnings("unchecked")
        public <T> T[] toArray(T[] a) {
            int size = size();
            if (a.length < size)
                return Arrays.copyOf(this.a, size,
                                     (Class<? extends T[]>) a.getClass());
            System.arraycopy(this.a, 0, a, 0, size);
            if (a.length > size)
                a[size] = null;
            return a;
        }

        @Override
        public E get(int index) {
            return a[index];
        }

        @Override
        public E set(int index, E element) {
            E oldValue = a[index];
            a[index] = element;
            return oldValue;
        }

        @Override
        public int indexOf(Object o) {
            E[] a = this.a;
            if (o == null) {
                for (int i = 0; i < a.length; i++)
                    if (a[i] == null)
                        return i;
            } else {
                for (int i = 0; i < a.length; i++)
                    if (o.equals(a[i]))
                        return i;
            }
            return -1;
        }

        @Override
        public boolean contains(Object o) {
            return indexOf(o) != -1;
        }

        @Override
        public Spliterator<E> spliterator() {
            return Spliterators.spliterator(a, Spliterator.ORDERED);
        }

        @Override
        public void forEach(Consumer<? super E> action) {
            Objects.requireNonNull(action);
            for (E e : a) {
                action.accept(e);
            }
        }

        @Override
        public void replaceAll(UnaryOperator<E> operator) {
            Objects.requireNonNull(operator);
            E[] a = this.a;
            for (int i = 0; i < a.length; i++) {
                a[i] = operator.apply(a[i]);
            }
        }

        @Override
        public void sort(Comparator<? super E> c) {
            Arrays.sort(a, c);
        }
    }

 

1、虽然是new ArrayList(),但方法返回值声明是List,所以此方法的结果是List,要声明为List:

List<String> myList = Arrays.asList(myArray);  

不能换为ArrayList

 

 

2、它new的这个ArrayList,不是集合中ArrayList,而是Arrays的内部类ArrayList。

虽然表面上是List,但底层仍是数组:

private final E[] a;  //E是泛型

 

 

3、再看一下这个内部类的构造函数:

ArrayList(E[] array) {
   a = Objects.requireNonNull(array);
}

直接传的数组,java只有值传递(浅拷贝),传递引用类型时传的是地址,操作的其实就是实际的数组(传入的那个数组)。

内部类ArrayList中的数组只有一个元素,这个元素就是传入的数组,所以调用size(),返回值是1;get(0)返回的是传入的数组;get(1)报错,显示下标超出范围,因为下标只有0。

 

 

4、看一下这个内部类的继承关系图:

private static class ArrayList<E> extends AbstractList<E>  //这个内部类继承了AbstractList
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E>  //AbstractList又implements List

就是说内部类ArrayList implements List,这个List就是集合中的那个List接口,定义了add()、remove()、set()、get()等一些列操作List集合的方法。

我们看看上面内部类ArrayList的源码,只实现了set()、get()等方法,并没有实现add()、remove()、clear()之类的方法,

所以这个内部类的对象可以使用add()、remove()这些方法,有代码提示、也可以通过编译,因为implments List,这些方法定义都有;但一运行会报运行时异常,因为这些方法都是抽象的,内部类ArrayList没有提供实现。

 

 

 

5、内部类ArrayList显然很鸡肋,如何转换为集合中的ArrayList类?

方法很多,下面这种是最简单的:

ArrayList list = new ArrayList<>( Arrays.asList("a", "b", "c") )  //使用集合中ArrayList类的构造函数,传入内部类ArrayList对象即可

怎么知道ArrayList是内部类还是集合中的那个?我们看一下内部类的声明:

private static class ArrayList<E>

private,内部类ArrayList只能在Arrays中使用,所以上面的ArrayList是集合中的那个。

也正是因为private,List<String> myList = Arrays.asList(myArray);   这个方法的返回值要声明为List,不能声明为内部类ArrayList(Arrays类外不能使用此内部类)。

 

 

数组转List集合的完整方式:

String[] myArray = { "Apple", "Banana", "Orange" };  //数组
List<String> myList = Arrays.asList(myArray);  //内部类ArraysList,中间人,桥梁

//以上2句代码也可以写为
List<String> myList = Arrays.asList( "Apple", "Banana", "Orange" );
ArrayList list = new ArrayList<>( mylist );  //集合中的ArrayList

 

 

数组反转:

        String[] arr= {"gailun", "huangzi", "zhaoxin"};
        List<String> list = Arrays.asList(arr); //将数组转换为内部类ArrayList
        Collections.reverse(list); //调用Collections工具类的静态方法实现数组反转,数组已被修改

        System.out.println(list); //[zhaoxin, huangzi, gailun]

        for (String s1:arr)
            System.out.println(s1);
        
        // zhaoxin
        // huangzi
        // gailun

上面已经说过,创建内部类ArrayList对象时,传的是数组地址,操作的就是数组本身,对ArrayList的反转就是对数组的反转。

 

 

不管是使用增强for循环、还是使用while+iterator遍历集合|数组,都不能在循环中增、删集合|数组元素,因为:

1、操作的是临时变量,并不是原本的数组元素,只能进行读操作

2、增、删元素后,迭代次数发生改变,与原本的迭代次数不一致,会导致迭代出错

 

以上是关于Java 基础知识的一些易错点的主要内容,如果未能解决你的问题,请参考以下文章

js基础知识易错点

java基础中的易错点

Java代码实际应用中的易错点记录

C++基础知识 易错点 总结(待补)

C++基础知识 易错点 总结(待补)

JavaGuide易错点总结