Java数据类型
数据类型
Integer
1.Integer的比较
- 基本型和基本型封装型进行 “ == ” 比较,基本型封装型将会自动拆箱成为基本型后,再去比较。
int a = 220; Integer b = 200; System.out.println(a == b); //true
- 两个 Integer 类型进行 “ == ” 比较,如果其值在 [-128,127] ,那么返回 ture,否则返回 false。
Integer a = 3; Integer b = 3; Integer c = 321; Integer d = 321; System.out.println(a == b); //true System.out.println(c == d); //false,大于127
- 两个基本型的封装型进行 equals() 比较,首先 equals() 会比较,如果类型相同,则继续比较值,如果值也相同,返回 true。
Integer a = 1; Integer b = 2; Integer c = 3; System.out.println(c.equals(a + b)); //true
- 基本型封装性调用equals(),但是参数是基本类型,这时候会先进行自动装箱,基本型转换为其封装类型,再进行 3 中的比较
int i = 1; int j = 2; Integer c = 3; System.out.println(c.equals(i + j)); //true
- 同样被 new 出来的相同类型的对象一定是不 " == " 的,因为 " == " 比较的是位置,而 new 出来的对象一定是全新的地址。但是用 equals() 比较时是比较的数值,是相等的。
Integer a = new Integer(5); Integer b = new Integer(5); System.out.println(a == b); //false System.out.println(a.equals(b)); // true
- 若两个 Byte 对象相加,则会自动升格为 int , 只能赋值给 int 或更到精度的类型,因此赋值给 Byte 一定会出错。
Byte a = new Byte( (byte)5 ); Byte b = new Byte( (byte)5 ); Byte c = a + b; //报错
2.缓存池
new Integer(123) 与 Integer.valueOf(123) 的区别在于:
- new Integer(123) 每次都会新建一个对象;
- Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
Integer x = new Integer(123); Integer y = new Integer(123); System.out.print(x == y); //false Integer z = Integer.valueOf(123); Integer k = Integer.valueOf(123); System.out.print(z == k); //true
valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容,在 Java 8 中,Integer 缓存池的大小默认为 -128~127。
public static Integer valueOf(int i) { if (i >= IntegerCache.low && i < IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
编译器会在自动装箱过程调用 valueOf() 方法,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。
Integer m = 123; Integer n = 123; System.out.print(m == n); //truel
基本类型对应的缓冲池如下:
- boolean values true and false
- all byte values
- short values between -128 and 127
- int values between -128 and 127
- char in the range \u000 to \u007F
在使用这些基本类型对应的包装类型时,就可以直接使用缓冲池的对象
String
1.不变性
String 这种不可变类,类值一旦被初始化,就不能在被改变了,如果被修改,将会是新的类。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; ...... }
上方源码总结出两点:
String 被 final 修饰,说明 String 类绝不可能被继承,任何对 String 的操作方法,都不会被继承覆写。
String 中保存数据的一个 char 的数组 value。value 也被 final 修饰,所以 value 一旦被赋值,内存地址绝对无法被修改。而且 value 的权限是 private,外界访问不到,String 也没有开放对 value 赋值的方法,所以 value 一旦产生,内存地址根本无法被修改。
String s1 = "hello"; String s2 = "hello"; String s3 = new String("hello"); String s4 = new String("hello"); //相同的字符串常量,java编译器只创建一个,所以返回true System.out.print(s1 == s2); System.out.print(s1 == s3); //false 不同得对象 System.out.print(s3 == s4); //false s1 = s1 + "world"; //输出helloworld,但是已经不是原来的对象了,s1指向新的内存空间 System.out.println(s1); String a = "hello2"; final String b = "hello"; String d = "hello"; String c = b + 2; String e = d + 2; System.out.println( (a == c) ); //true System.out.println( (a == e) ); //false //final变量是基本数据类型以及String类型时,如果在编译期间能知道它的确切值,则编译器会把它当做编译器常量使用。
不可变的好处:
- 可以缓存 hash 值: 因为String 的 hash 值经常被使用,例如 String 用作 HashMap 的 key。不可变的特性使得 hash 值也不可变,因此只需要进行一次计算
- String pool 需要:如果一个 String 对象已经被创建过了,那么就会从 String Pool 中去的引用,只有 String 不可变才能使用 String pool
- 安全性:String 经常作为参数,String 不可变性可以保证参数不可变。
- 线程安全:String不可变性天生具备线程安全
2.字符串乱码
主要因为在二进制转化操作时,并没有强制规定文件编码,而不同的环境默认的文件编码是不一致的。
String str = "nihao 你好 喬亂"; //字符串转化成 byte 数组 byte[] bytes = str.getBytes("ISO-8859-1"); //byte数组转化成字符串 String s2 = new String(bytes); log.info(s2); //打印结果为: nihao ?? ??
就算把代码修改成 String s2 = new String(bytes, "ISO-8859-1"); 也不行。主要是因为 ISO-8859-1 对中文的支持有限,唯一的方法是都统一使用 UTF-8。
总结乱码根源:
- 字符集不支持复杂汉字
- 二进制转化时字符集不匹配
解决办法:
- 所有可以指定字符集的地方指定字符集,比如 new String 和 getBytes 这两个地方。
- 我们应该使用 UTF - 8 这种完整支持复杂汉字的字符集
3.首字母大小写
首字母小写的这时候我们一般都会这么做:
name.substring(0, 1).toLowerCase() + name.substring(1); //使用 substring 方法,截取字符串连续的一部分。
substring 的两个方法,如下。substring 方法的底层使用的是字符数组的截取方法: *Array.copyOfRange(char数组,开始位置,结束位置); * char数组中进行一段范围的拷贝。
//从开始位置到截取到结束位置 public String substring(int beginindex, int endindex) //从开始截取到文本末尾 public String substring(int beginindex)
相反如果要修改成后字母大写,则:
name.substring(0,1).toUpperCase() + name.substring(1);
4.相等判断
相等判断有两种, equals 和 equalsIgnoreCase 。后者判断相等会忽略大小写。下面是 equals 源码,结论是判断是否相等应当从底层结构出发。
public boolean equals(Object anObject) { //判断地址是否相同 if (this == anObject) { return true; } //比较对象如果不是String,直接返回false if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; //比较字符串长度是否相等,不等直接返回false if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; //依次比较每个字符是否相等 while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
5.替换删除
替换在工作中也经常使用,有三种场景。
- replace 替换所有字符、
- replaceAll 批量替换字符串、
- replaceFirst 替换遇到的第一个字符串。
其中在使用 replace 时需要注意,replace 有两个方法,一个入参是 char,一个入参是 String,两者就是单引号和多引号的区别。
name.replace('a','b') ,//表示替换所有字符 name.replace("a","b") ,//表示替换所有字符串
- 当然删除某些字符,也可使用 replace 方法,把想删除的字符替换成 " " 即可
- replace 并不只是替换一个,是替换所有匹配到的字符或字符串哦。
public void testReplace(){ String str ="hello word !!"; log.info("替换之前 :{}",str); str = str.replace('l','d'); log.info("替换所有字符 :{}",str); str = str.replaceAll("d","l"); log.info("替换全部 :{}",str); str = str.replaceFirst("l",""); log.info("替换第一个 l :{}",str); }
替换之前 :hello word !! 替换所有字符 :heddo word !! 替换全部 :hello worl !! 替换第一个 :helo worl !!
6.拆分和合并
拆分我们使用 split 方法,该方法有两个入参数。第一个参数是我们拆分的标准字符,第二个参数是一个 int 值,叫 limit,来限制我们需要拆分成几个元素。如果 limit 比实际能拆分的个数小,按照 limit 的个数进行拆分。
String s ="boo:and:foo"; // 我们对 s 进行了各种拆分,演示的代码和结果是: s.split(":"); //结果:["boo","and","foo"] s.split(":",2); // 结果:["boo","and:foo"] s.split(":",5); // 结果:["boo","and","foo"] s.split(":",-2); // 结果:["boo","and","foo"] s.split("o"); // 结果:["b","",":and:f"] s.split("o",2); // 结果:["b","o:and:foo"]
从结果来看,limit 对拆分的结果,是具有限制作用的,还有就是拆分结果里面不会出现被拆分的字段。那如果字符串里面有一些空值呢,拆分的结果如下:
String a =",a,,b,"; a.split(",") //结果:["","a","","b"]
从拆分结果中,我们可以看到,空值是拆分不掉的,仍然成为结果数组的一员,如果我们想删除空值,只能自己拿到结果后再做操作,但 Guava(Google 开源的技术工具) 提供了一些可靠的工具类,可以帮助我们快速去掉空值,如下:
// Splitter 是 Guava 提供的 API List<String> list = Splitter.on(',') .trimResults()// 去掉空格 .omitEmptyStrings()// 去掉空值 .splitToList(a); log.info("Guava 去掉空格的分割方法:{}",JSON.toJSONString(list)); // 打印出的结果为: ["a","b c"]
7.String,StringBuffer,StringBuilder
可变性
- String不可变
- StringBuffer 和 StringBulider 可变
线程安全
- String 不可变,所以线程安全
- StringBulider 线程不安全
- StringBuffer 线程安全,内部使用synchronized进行同步
8.String Pool
字符串常量池(String Pool) 保存着所有的字符串字面量( literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern 方法在运行过程中将字符串添加到 String Pool 中。
当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会 在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。
下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 方法取得一个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中,然后返回这个字符串引用。因此 s3 和 s4 引用的是同一个字符串。
String s1 = new String("aaa"); String s2 = new String("aaa"); System.out.print(s1 == s2); //false String s3 = s1.intern(); String s4 = s1.intern(); System.out.print(s3 == s4); //true //如果是采用 "bbb" 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中 String s5 = "bbb"; String s6 = "bbb"; System.out.print(s5 == s6); //true
在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。
9.new String("abc")
使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 "abc" 字符串对 象)。
- "abc" 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个"abc" 字符串字面量。
- 而使用 new 的放实会在堆中创建一个字符串对象。
//创建一个测试类 public class NewStringTest { public static void main(String[] args) { String s = new String("abc"); } }
使用 javap -verbose 进行反编译,得到以下内容:
1. // ... 2. Constant pool: 3. // ... 4. #2 = Class #18 // java/lang/String 5. #3 = String #19 // abc 6. // ... 7. #18 = Utf8 java/lang/String 8. #19 = Utf8 abc 9. // ... 10. 11. public static void main(java.lang.String[]); 12. descriptor: ([Ljava/lang/String;)V 13. flags: ACC_PUBLIC, ACC_STATIC 14. Code: 15. stack=3, locals=2, args_size=1 16. 0: new #2 // class java/lang/String 17. 3: dup 18. 4: ldc #3 // String abc 19. 6: invokespecial #4 // Method java/lang/String ."<init>":(Ljava/lang/String;)V 20. 9: astore_1 21. // ...
在 Constant Pool 中,#19 存储这字符串字面量 "abc",#3 是 String Pool 的字符串对象, 它指向 #19 这个字符串字面量。在 main 方法中,0: 行使用 new #2 在堆中创建一个字符串 对象,并且使用 ldc #3 将 String Pool 中的字符串对象作为 String 构造函数的参数。
//以下是 String 构造函数的源码,可以看到,在将一个字符串对象作为另一个字符串对象的构 造函数参数时,并不会完全复制 value 数组内容,而是都会指向同一个 value 数组。 public String(String original) { this.value = original.value; this.hash = original.hash; }
Long
1.缓存
Long 被我们关注的就是 Long 的缓存问题,Long 自己实现了一种缓存机制,缓存了从-128 到 127 内的所有 Long 值,如果是这个范围内的 Long 值,就不会初始化,而是从缓存中拿。源码如下:
private static class LongCache { private LongCache(){} // 缓存,范围从 -128 到 127,+1 是因为有个 0 static final Long cache[] = new Long[-(-128) + 127 + 1]; // 容器初始化时,进行加载 static { // 缓存 Long 值,注意这里是 i - 128 ,所以再拿的时候就需要 + 128 for(int i = 0; i < cache.length; i++) cache[i] = new Long(i - 128); } }
2.面试题
- 为什么使用 Long 时,大家推荐使用 valueOf 方法,少使用 parseLong 方法?
因为 Long 本身有缓存机制,缓存了 - 128 到 127 范围的 Long,valueOf 方***从缓存中去拿值,如果命中缓存,会减少资源的开销,parseLong 方法就没有这个机质。
float 和 double
Java 不能隐式执行向下转型,因为这会使得精度降低。
1.1 字面量属于 double 类型,不能直接将 1.1 直接赋值给 float 变量,因为这是向下转型。 //float f = 1.1; 1.1f 字面量才是 float 类型。 float f = 1.1f;
隐式类型转换
因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型下转型为 short 类型。
short s1 = 1; // s1 = s1 + 1; 错误的写法
但是使用 += 或者 ++ 运算符可以执行隐式类型转换
s1 += 1; //正确 s1++; //正确 //上面的语句相当于如下 s1 = (short) (s1 + 1);
switch
- 从 Java 7 开始,switch可以使用 String 对象。
- switch 不支持 long,是因为 switch 的设计初衷是对那些只有少数的几个值进行等值判断,如果值过于复杂,还是 if 比较合适。