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、增、删元素后,迭代次数发生改变,与原本的迭代次数不一致,会导致迭代出错
6、枚举的使用
在java5中引入了枚举,枚举是一种特殊的类,与其它类不同,枚举使用关键字enum,不使用关键字class。
枚举常用来代替一组常量,比如4个季节:
public enum Season { SPRING, //逗号,一般全大写 SUMMER, AUTUMN, WINTER; //分号 }
把enum当做class即可。enum其实就是一种特殊的class,使用enum修饰会自动继承java.lang.Enum类。
用户的状态(在线、离线、忙碌、Q我吧)、
用户的身份(普通用户、VIP、SVIP)、
订单状态(已付款、已发货、待取件、待收货、待退款)、
商品库存状态(库存充足、即将售罄、缺货)、
后台管理的角色(普通管理员、最高级别管理员)、
登录角色(教职工、学生、游客).......
等等,需要用一组常量来表示的,都可以使用枚举。
枚举可以单独存在(就像类一样,单独写成一个文件),也可以内嵌在某个类中(相当于内部类):
public class Test { private Season season; //声明为枚举类型 // season=Season.SPRING; //赋值 public enum Season { //相当于内部类的形式 SPRING, SUMMER, AUTUMN, WINTER; } public void getSeason1(Season season){ //实用==与枚举值进行比较 if (season==Season.SPRING) System.out.println("now is spring"); } public void getSeason2(Season season){ //在switch中使用枚举 switch (season){ case SPRING: System.out.println("now is spring"); break; case SUMMER: System.out.println("now is summer"); break; case AUTUMN: System.out.println("now is autumn"); break; case WINTER: System.out.println("now is winter"); break; } } }
比如说订单状态可以作为枚举,写在Order类中,也可以把订单状态单独拿出来,写在一个单独的.java文件中。