Java数据类型

数据类型

Integer

1.Integer的比较
  1. 基本型和基本型封装型进行 “ == ” 比较,基本型封装型将会自动拆箱成为基本型后,再去比较。
int a = 220;
Integer b = 200;
System.out.println(a == b); //true
  1. 两个 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
  1. 两个基本型的封装型进行 equals() 比较,首先 equals() 会比较,如果类型相同,则继续比较值,如果值也相同,返回 true。
Integer a = 1;
Integer b = 2;
Integer c = 3;
System.out.println(c.equals(a + b));    //true
  1. 基本型封装性调用equals(),但是参数是基本类型,这时候会先进行自动装箱,基本型转换为其封装类型,再进行 3 中的比较
int i = 1;
int j = 2;
Integer c = 3;
System.out.println(c.equals(i + j));    //true
  1. 同样被 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
  1. 若两个 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.面试题

  1. 为什么使用 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

  1. 从 Java 7 开始,switch可以使用 String 对象。
  2. switch 不支持 long,是因为 switch 的设计初衷是对那些只有少数的几个值进行等值判断,如果值过于复杂,还是 if 比较合适。
全部评论

相关推荐

04-13 18:10
门头沟学院 Java
想熬夜的小飞象在秋招:被腾讯挂了后爸妈以为我失联了
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务