疯狂Java笔记之表达式中的陷阱

关于字符串的陷阱

JVM对字符串的处理

首先看如下代码:

String java=new String("HelloJack");

上面创建了两个字符串对象,其中一个是“HelloJack”这个直接量对应的字符串对象,另一个是由new String()构造器返回的字符串对象。

对于Java程序中的字符串直接量,JVM会使用一个字符串池来保存它们:当第一次使用某个字符串直接量是,JVM会将它放入字符串池进行缓存。在一般情况下,字符串池的字符串对象不会被垃圾回收,当程序再次需要使用该字符串时,无需重新创建一个新的字符串,而是直接让引用变量指向字符串池中已有的字符串。如下代码:

String str1="Hello Java";
String str2="Hello Java";
System.out.println(str1==str2); 

因为str1和str2都是直接量,都指向JVM字符串池里的“Hello Java”字符串,所以为true;

除了直接创建之外,也可以通过字符串连接表达式创建字符串对象,因此可以将一个字符串连接表达式赋给字符串变量。如果这这个字符串连接表达式的值可以在编译时确定下来,那么JVM会在编译时计算该字符串变量的值,并让它指向字符串池中对应的字符串。如下代码:

String str1="HelloJava";
String str2="Hello"+"Java";
System.out.println(str1==str2); 

最终结果返回就是true.需要注意的是上面都是直接量,而没有变量,没有方法的调用。因此,JVM可以在比编译时就确定该字符串连接表达式的值,可以让该字符串变量指向字符串池中对应的字符串。但如果程序使用了变量,或者调用的方法,那么只能等到运行时才能确定该字符串连接表达式的值,也就无法再编译时确定该字符串变量的值,因此无法利用JVM的字符串池。如下代码:

String str1="HelloJava9";
String str2="Hello"+"Java9";
System.out.println(str1==str2); 
String str3="HelloJava"+"HelloJava".length();
System.out.println(str1==str3);

第一个返回了true,第二个输出返回了false;

当然还有一个情况例外的,就是当变量执行“宏替换”时也是可以让字符串变量指向JVM字符串池中对应字符串。如下代码:

String str1="HelloJava9";
String str2="Hello"+"Java9";
System.out.println(str1==str2); 
final int len=9;
String str3="HelloJava"+len;
System.out.println(str1==str3);

不可变的字符串

String类是一个典型的不可变类。当一个String对象创建完成后,该String类里包含的字符序列就被固定下来,以后永远都不会改变。如下代码:

String str="Hello";
System.out.println(System.identityHashCode(str));
str=str+"Java";
System.out.println(System.identityHashCode(str));

当一个String对象创建完成后,该String里包含的字符序列不能改变。可能会有疑惑,str变量对应的字符序列不是一直在变吗,当时str只是一个引用类型变量。像C语言的指针,他并不是真正的String对象,只是指向String对象而已。 示意图如下:

string.PNG

string2.PNG

从图中知道"Hello”字符串也许以后永远都不会再被用到了,但是这个字符串并不会被垃圾回收掉,因为它一直存在于字符串池中,这也是Java内存泄露的原因之一。

对于一个String类而言,他代表字符序列不可改变的字符串,因此如果程序需要一个字符序列会发生改变的字符串,那么应该考虑使用StringBuilder和StringBuffer.

在通常情况下优先考虑使用StringBuidler.StringBuidler与StringBuffer的区别在于,StringBuffer是线程安全的,也就是说StringBuffer类里的绝大部分方法都增加了synchoronized修饰符。对方法增加synchoronized修饰符可以保证该方法线程安全,当会降低该方法的执行效率。在没有多现场的环境下,应该优先使用StringBuilder来表示字符串。

字符串比较

如果程序需要比较两个字符串是否哦相同,用==进行判断就可以了;但是如果判断两个字符串所包含的字符序列时候相同,则应该用String重写过的equals()方法进行比较。假如没有重写equals方法,则比较的是引用类型的变量所指向的对象的地址。

表达式类型的陷阱

表达式类型的自动提升

Javc语言规定:当一个算术表达式中包含多个基本类型的值时,整个算术表达式的数据类型将自动提升。java语言的自动提升规则如下:

  • 所有的byte类型,short类型和char类型将被提升到int类型。
  • 整个算术表达式的数据类型自动提升与表达式中的最高等级操作数同样的类型。操作数的如下,位于箭头右边的类型等级高于位于箭头左边的类型等级。

类型提升.PNG

复合赋值运算符的陷阱

经过前面的介绍,可以知道下面的是错误的:

short sValue=5;
sValue=sValue-2;

因为sValue将自动提升为int类型,所以程序将一个int类型的值赋值给short类型的变量时导致了编译错误。 但是改为如下就没有问题了:

short sValue=5;
sValue-=2;

上面程序使用复合赋值运算符,就不会导致编译错误。 实际上sValue-=2;等价于sValue=(sValue的类型)(sValue-2),这就是复合赋值运算符的隐式类型转换。

如果结果值的类型步变量的类型大,那么复合赋值运算符将会执行一个强制类型转换,这个强制类型转换将有可能导致高位“截断”,如下代码所示:

short st=5;
st+=10;
st+=90000;
System.out.println(st);

为了避免这种潜在的危险,有如下几种情况下需要特别注意:

  • 将复合赋值运算符运用于byte,short或char等类型的变量
  • 将复合赋值运算符运用于int类型的变量,而表达式右侧是long,float或double类型的值。
  • 将复合赋值运算符运用于float类型的变量,而表达式右侧是double类型的值。

二进制整数

int it=ob1010_1010;
byte bt=(byte)ob1010_1010;
System.out.println(it==bt);

it和bt是不相等的,造成这种问题的原因在于这两条规则:

  • 直接使用整数直接量时,系统会将它当成int类型处理。
  • byte类型的整数虽然可以包含8位,但最高位是符号位。

转义字符的陷阱

Java程序提供了三种方式来表示字符。

  • 直接使用单引号括起来的字符值。如‘a’.
  • 使用转义字符,如‘\n’.
  • 使用Unicode转义字符,如‘\u0062’.

java对待Unicode转义字符时不会进行任何处理,它会将Unicode转义字符直接替换成对应的字符,这将给java程序带来一些潜在的陷阱。

慎用字符Unicode转义形式

理论上,Unicode转义字符可以代表任何字符(不考虑那些不在Unicode码表内的字符),因此很容易想到:所有字符都应该可以使用Unicode转义字符的形式。为了了解Unicode转义字符带来的危险,来看如下程序:

System.out.println("abc\u000a".length());

表面上看程序将输出4当编译该程序时发现程序无法通过编译。原因是Java对Unicode转义字符不会进行任何特殊处理,它只是简单的将Unicode转义字符替换成相应的字符。对于\u000a而言,他相当于一个换行符(\n),因此对Java编译器而言,上面代码相当于如下:

System.out.println("abc\n".length);

中止行注释的转义字符

在java程序中使用\u000a时,它将被直接替换成换行字符(相当于\n),因此java注释中使用这个Unicode转义字符要特别小心

泛型可能引起的错误

原始类型变量的赋值

在严格的泛型程序中,使用泛型声明的类时应该总是为之指定类型实参,但为了与老的Java代码保存一致,Java也允许使用带泛型声明的类是不指定类型参数,如果使用带泛型声明的类时没有传入类型实参,那么这个类型参数默认是声明该参数时指定的第一个上限类型,这个类型参数也被称为raw type(原始类型)

当尝试把原始类型的变量赋给带泛型类型的变量时,会发生一些有趣的事情,如下代码:

List list=new ArrayList<>(); 
list.add("Hello");
list.add("Jack");
list.add("xie");
List<Integer> intList=list;
for(int i=0;i<intList.size();i++){
    System.out.println(intList.get(i));
}

上面代码编译正常,并且正常输出intList的集合是三个普通的字符串。通过上面可以看出:当程序把一个原始类型的变量赋给一个带泛型信息的变量时,只要他们的类型保持兼容,无论List集合里实际包含什么类型的元素,系统都不会有任何问题。 不过虽然我们编译的时候可能不会有什么问题,但是当我们把元素拿出来处理的时候intList还是引用的是String类型,而不是Integer,因此运行时可能还是会出问题。而当我们String in=intList.get(i)时是会报编译错误的。 为此总结如下:

  • 当程序把一个原始类型的变量赋给一个带泛型信息的变量时,总是可以通过编译---只是会提示一些警告信息。
  • 当程序试图访问带泛型声明的集合的集合元素时,编译器总是把集合元素当成泛型类型处理---它并不关心集合里集合元素的实际类型。
  • 当程序试图访问带泛型声明的集合的集合元素是,JVM会遍历每个集合元素自定执行强制类型转换,如果集合元素的实际类型与集合所带的泛型信息不匹配,运行时将引发ClassCastException异常。

原始类型带来的擦除

Apple<Integer> apple=new Apple<Integer>();  
Integer as=apple.getSize();
Apple b=apple;
Number size1=b.getSize();
Integer size2=b.getSize();

Integer size2=b.getSize();时代码会报错。

当一个带泛型信息的Java对象赋给不带泛型信息的变量时,Java程序会发生擦除,这种擦除不仅会擦除使用Java类时传入的类型实参,而且会擦除所有的泛型信息,也就是擦除所有尖括号里的信息。

创建泛型数组的陷阱

List<String>[] lsa=new List<String>[10];

编译上面的代码会提示‘创建泛型数组’的错误,这正是由Java引起运行时异常,这就违背了Java泛型的设计原则————如果一段代码在编译时系统没有产生“[unchecked]未经检查的转换”警告,则程序在运行时不会引发ClassCastException异常。 再看如下代码:

public class GenericArray<T>{
    class A{}
    public GenericArray(){
        A[]  as=new A[10];
    }
}

上面编译还是会错,A[] as=new A[10]只是创建A[]数组,而没哟创建泛型数组,因为内部类可以直接使用T类形形参,因此可能出现如下形似:

public class GenericArray<T>{
    class A{
        T foo;
    }
    public GenericArray(){
        A[]  as=new A[10];
    }
}

这就导致创建泛型数组了,违背Java不能创建泛型数组的原则,所以JDK设计还是比较谨慎的。

正则表达式的陷阱

String str="java.is.funny";
String strAttr=str.split(".");
for(String s:strAttr){
    System.out.println(s);
}

上面程序包含多个点号(.)的字符串,接着调用String提供的split()方法,以点号(.)作为分割符来分割这个字符串,希望返回该字符串被分割后得到的字符串数组。运行该程序,结果发现什么都没有输出。 对于上面程序需要注意如下两点:

  • String提供的split(String regex)方法需要的参数是正则表达式
  • 正则表达式中的点号(.)可匹配任意字符。 将上面代码改为如下形式String strAttr=str.split("\\.");即可实现分割。

String类也增加了一些方法用于支持正则表达式,具体方法如下:

  • matches(String regex):判断该字符串是否匹配指定的正则表达式。
  • String replaceAll(String regex,String replacement):将字符串中所有匹配指定的正则表达式的子串替换成replacement后返回。
  • String replaceFirst(String regex,String replacement):将字符串中第一个匹配指定的正则表达式的子串替换replacement后返回。
  • String[] split(String regex):以regex正则表达式匹配的子串作为分隔符来分割该字符串。

以上方法都需要一个regex参数,这个参数是正则表达式。因此使用的时候要小心。

多线程的陷阱

不要调用run方法

Java提供了三种方式来创建,启动多线程。

  • 继承Thread类来创建线程类,重写run()方法作为线程执行体。
  • 实现Ruannable接口来创建线程类,重写run()方法作为线程执行体。
  • 实现Callable 接口来创建线程类,重写call()方法作为线程执行体。

其中第一种方式的效果最差,它有两点坏处:

1.线程类继承了Thread类,无法再继承其他父类。 2.因为每条线程都是Thread子类的实例,因此可以将多条线程的执行流代码于业务数据分离。

对于第二种和第三种方式,它们的本质是一样的,只是Callable接口里包含的call()方法既可以声明抛出异常,也可以拥有返回值。

静态的同步方法

public class SynchronStatic implements Runnable{
    
    static boolean staticFlag =true;
    
    public static synchronized void test(){
        for(int i=0;i<100;i++){
            System.out.println("test0:"+Thread.currentThread().getName()+" "+i);
        }
    }
    
    public void test1(){
        synchronized (this) {
            for(int i=0;i<100;i++){
            System.out.println("test1:"+Thread.currentThread().getName()+" "+i);
            } 
        }
    }
    
    @Override
    public void run() {
        if(staticFlag){
            staticFlag=false;
            test();
        }else{
            staticFlag=true;
            test1();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        SynchronStatic synchronStatic=new SynchronStatic();
        new Thread(synchronStatic).start();
        new Thread(synchronStatic).start();
    }

}

运行结果如下:

thread.PNG

上面的代码用了Synchronized怎么还会一起执行呢。因为第一条线程锁定的是SynchronStatic类,而不是synchronStatic所引用的对象,而第二条线程完全可以获得对synchronStatic所引用的对象的锁定,因此系统可以切换到执行第二条线程。假如我们把上面中的同步代码块的同步监视器改为SynchronStatic类,如下形式:

public void test1(){
    synchronized (SynchronStatic.class) {
        for(int i=0;i<100;i++){
        System.out.println("test1:"+Thread.currentThread().getName()+" "+i);
        } 
    }
}

此时静态同步方法和当前类为同步监视器的同步代码块不能同时执行。

静态初始化启动心线程执行初始化

静态初始化快中的代码不一定是类初始化操作,静态初始化中启动线程run()方法代码只是新线程执行体,并不是类初始化操作。类似的,不要认为所有放在非静态初始化块中的代码就一定是对象初始化操作,非静态初始化块中启动新线程的run()方法代码只是新线程的线程执行体,并不是对象初始化操作。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏程序生活

Python中defaultdict用法

2036
来自专栏Java学习网

Java面试题系列之基础部分(二)——每天学5个问题

Java基础部分学习的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语法,集合的语法,io的语法,虚拟机方面的语法,这些都是最基...

2645
来自专栏Golang语言社区

【Go 语言社区】Go语言类型转换

类型转换是一种可变从一种数据类型转换成另一种数据类型。例如,如果要存储一个long值转成一个简单的整数,那么可以强制类型转换long为int。可以从一种类型使用...

36214
来自专栏数据结构笔记

数据结构(四):栈的应用之表达式求值

用户从控制台输入一个数学表达式(所有输入均合法),数学表达式只包含四则运算,程序需输出表达式对应的结果,如:

852
来自专栏Java帮帮-微信公众号-技术文章全总结

04.Java对象和类

04.Java对象和类 Java 对象和类 Java作为一种面向对象语言。支持以下基本概念: 多态 继承 封装 抽象 类 对象 实例 方法 重载 本节我们重点研...

4486
来自专栏小古哥的博客园

JS数组去重的三种方法

在程序中,通常解决一个问题的方法有很多种。当然这些不同思路的解决方法,在性能和效率上也有很大差异。 以下是数字去重的三种方法, 一、循环遍历法(传统思路) 最简...

3755
来自专栏Spark学习技巧

scala中常用但其他语言不常见的符号含义 - 心灵空谷幽兰 - 博客园

本文旨在介绍Scala在其他语言中不太常见的符号含义,帮助理解Scala Code。 随着我对Scala学习的深入,我会不断增加该篇博文的内容。 修改记录 -...

2016
来自专栏MasiMaro 的技术博文

结构体和类

在C++中类与结构体并没有太大的区别,只是默认的成员访问权限不同,类默认权限为私有,而结构体为公有,所以在这将它们统一处理,在例子中采用类的方式。

2042
来自专栏fangyangcoder

C++学习笔记——大杂烩

                                                        by方阳

1052
来自专栏郭耀华‘s Blog

排序算法

冒泡排序法 比较相邻的元素。如果第一个比第二个大,就交换他们两个。 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大...

2846

扫码关注云+社区

领取腾讯云代金券