Java数据类型

本文深入解析Java中的整型、字符型、字符串及Integer等基础类型的特点与操作方式,包括数值范围、不可变性、字符串拼接及常量池机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、整型:byte、short、int、long

1、byte

byte类型有8位,第1位作为符号位,0为正数,1为负数,剩余7位,有2*7=128种组合,因此负数有128个,正数也有128个,但由于0属于正数,因此取值范围是:-128~127

二、字符型:char

在Java语言规范中,§3.1有这样一句话,The Java programming language represents text in sequences of 16-bit code units, using the UTF-16 encoding(Java编程语言使用UTF-16编码来表示在16位代码单元中的文本)。关于Character的介绍中也说,Character literals can only represent UTF-16 code units (§3.1), i.e., they are limited to values from \u0000 to \uffff(字符型只能表示16位代码单元即2个字节,它们能表示的范围为\u0000 to \uffff)。由此可见,char只能表示2^16=65536个字符(注意:不能表示所有的字符),且范围为0X0000到0XFFFF。

使用char存放单个字符时,需加上单引号,如:char a = 'a';或者整数,如:char b = 123。

在进行运算时,如果有用单引号标记的字符,先找出该字符对应unicode编码的编号,然后进行相加。例如:char a1 = 'a' + 'b'; 计算过程:'a'对应的unicode编码是97,'b'是98,相加结果是195,而195对应的字符是Ã,因此a1的值为Ã

三、字符串:String

顾名思义,由若干个字符组成的字符数组,从如下源码可以看出

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0
    //......省略......
}

其中定义了一个字符数组char,用来存放字符串的值。

1、不可变性

说到字符串,不得不提它的“不可变性”,那么什么是不可变性呢?简单来说就是,一旦给String对象赋值,它的值就不能改变,那么它是怎么做到的呢?

a、长度不可变性:数组一旦赋值,本身的长度就固定且无法再改变;且加上final修饰符,数组的地址也无法改变

如果这个时候改变数组内部的内容呢,sorry,也无法改变

b、内容不可变性:private修饰的属性,不能在类的外部直接访问,并且该类没有向外暴露修改其值的公共方法

通过如下代码进行证明

String str = "abc";
//concat方法的作用是将指定的字符串拼接在该String末尾
str.concat("d");
System.out.println(str);

输出结果是:abc,但为什么不是abcd呢,因为String类型的对象是不可变的,该concat方法无法改变它的值,那如果代码改成这样呢?

String str = "abc";
//concat方法的作用是将指定的字符串拼接在该String末尾
str = str.concat("d");
System.out.println(str);

输出结果是:abcd,在解释原因之前,先看下concat方法的内部实现逻辑

public String concat(String str) {
   int otherLen = str.length();
   if (otherLen == 0) {
       return this;
   }
   int len = value.length;
   char buf[] = Arrays.copyOf(value, len + otherLen);
   str.getChars(buf, len);
   return new String(buf, true);
}

该方法在最后新建了一个String对象,并返回指向该对象的引用。然后再看上面的代码:str = str.concat("d"),将一个新的引用赋值给str,这个时候str指向的不再是"abc",而是"abcd"

同理,String类的方法:substring、replaceAll等都是通过新建一个新的String对象来实现的

不过针对内容不可变性,其实可以通过反射绕开这个限制,代码如下:

//定义一个字符数组并初始化
char[] chars = {'1','2','3'};
//通过字符数组构造一个String类对象
String str = new String(chars);
System.out.println("原始字符串的值为:" + str);
/**
 * 获取String类对象对应的Class类对象
 * Class类的对象代表运行中的Java应用的类和接口。
 * Class类没有公共构造方法,而是当类被类加载器加载后并且调用了defineClass,其对象自动被JVM创建。
 */
Class clazz = str.getClass();
//获取String类存储数据的字符数组value
Field valueFiled = clazz.getDeclaredField("value");
//设置可访问,否则下面的操作会报错
valueFiled.setAccessible(true);
//将字符数组value的内存地址赋值给charsCopy,方便下面对其内部内容进行操作
char[] charsCopy = (char[]) valueFiled.get(str);
//改变内部字符的值
charsCopy[0] = 'a';
charsCopy[1] = 'b';
charsCopy[2] = 'c';
System.out.println("改变后的字符串的值为:" + str);

输出结果为:

原始字符串的值为:123
改变后的字符串的值为:abc

2、操作符"+"

接下来对String对象重载的“+”操作符进行一些介绍,重载的意思是,一个操作符在应用于特定的类时,被赋予了特殊的意思(用于String的“+”与“+=”是Java中仅有的两个重载过的操作符)

首先看下面最简单的代码

public class Test {

    public static void main(String[] args) {
        String s1 = "a" ;
        String s2 = s1 + "b";
        System.out.println(s2);
    }
}

输出结果是ab,我们对这个结果一点都不意外,觉得跟操作字符类型一样,'a' + 'b' = 'ab',但不要忘了,“+”操作符一般只适用于对基本类型中的char、byte、short、int、long等进行操作,也能适用字符串其实是一件很神奇的事情,那么其内部具体是如何实现的呢?这个时候我们需要用到JDK自带的工具javap来反编译如上代码

1、javac Test.java,生成Test.class

2、javap -c Test.class,最终反编译的结果如下

重点看用黄色标记的部分,编译器自动引入了java.lang.StringBuilder类,并创建了该类的一个对象,然后调用了两次append方法,最后调用toString方法,返回一个新的String对象的引用,可以用如下代码演示整个流程:

StringBuilder sb = new StringBuilder();
sb.append("a").append("b");
String s3 = sb.toString();
s2 = s3 ;

既然当使用“+”操作符对字符串进行操作时,编译器默认使用了StringBuilder,那为什么在Java中建议不直接使用“+”而使用StringBuilder的append方法呢?至于原因,我们先看如下两段代码:

public static void main(String[] args) {
    //使用"+"生成String对象
    String s = "0" ;
    for (int i = 0; i < 100; i++) {
        s = s + i ;
    }
}

反编译的结果:

注意看用红色框圈起部分,它其实是一个循环体,每循环一次,都会创建一个新的StringBuilder对象,这样频繁的创建对象,是很影响性能和效率的。

接下来看另一种

public static void main(String[] args) {
    //使用"append方法"生成String对象
    StringBuilder sb = new StringBuilder("0");
    for (int i = 0; i < 100; i++) {
        sb.append(i);
    }
}

反编译的结果

可以看出,只创建了一个StringBuiidler对象,且循环体内的逻辑更清晰简短,这也就解释了为什么Java中建议不直接使用“+”而使用StringBuilder的append方法来对String进行操作。当然了,如果操作的次数不多,其实两者的差别不大,可以任意使用,但如果次数大的话,强烈建议使用StringBuilder

3、操作符"=="

下面再对操作符"=="使用在String中的场景进行分析,先看如下代码:

public static void main(String[] args) {
    String s1 = "ab" ;
    String s2 = "ab" ;
    System.out.println(s1 ==s2) ;//输出结果是true
}

首先解释下操作符"==",在基本数据类型时,该操作符对比的是值,而在引用类型中,对比的是引用地址。由于String是引用类型,且输出结果是true,那么就表示s1和s2指向的是同一个内存地址,那这是为什么呢?这时就引入了一个非常重要的String类特有的概念:字符串常量池(位于方法区中的一块内存区域)。当String s1 = "ab"被执行时,首先会在常量池中查找该值是否存在,如果不存在,将该值放入常量池中并返回内存地址给s1。当String s2 = "ab"被执行时,也会从常量池中去查找,这时该值已经存在,就直接返回该值的内存地址给s2,可以看出s1和s2指向的是同一块内存地址,所以用"=="比较的结果是true,如下是图示

接下来将代码改成如下:

public static void main(String[] args) {
    String s1 = "ab" ;
    //可以理解为字符的相加
    String s2 = "a" + "b" ;
    System.out.println(s1 ==s2) ;//输出结果是true
}

其实"a" + "b"和"ab"是一个意思,可以从反编译的结果中看出

也是使用到了上述将的常量池知识

再看这种

public static void main(String[] args) {
    String s1 = "ab" ;
    String s2 = "a" ;
    String s3 = s2 + "b" ;
    System.out.println(s1 ==s3) ;//输出结果是false
}

为什么结果是false呢?因为根据上面的讲解,String s3 = s2 + "b"最终的值虽然也是"ab",但实际上使用到了StringBuilder的toString方法,创建了一个新对象,s1和s3分别指向不同的内存地址

四、Integer

1、Integer作为基本数据类型int的包装类,有一些特性需要注意,看如下代码(将int类型的值转换为Integer)

public class IntegerTest {
    public static void main(String[] args) {
        //将int类型的值转换为Integer
        Integer i1 = 127 ;
        Integer i2 = 127 ;
        System.out.println(i1 == i2);//结果为true
        Integer i3 = 128 ;
        Integer i4 = 128 ;
        System.out.println(i3 == i4);//结果为false
    }
}

第一个判断为true,第二个为false,这个结果有点出乎意料,猜想在编译成.class文件过程中,进行了某些特殊操作,最简单的验证方法就是对.class文件进行反编译(注:除了使用jdk自带的javap命令,第三方jad工具更直观),反编译结果如下:

在这里可以看到,使用了Integer的valueOf方法,查看源码

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;

        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);

        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
}

可以看到调用valueOf方法后,首先判断该值如果是在IntegerCache.low和IntegerCache.high之间(即-128和127),则直接从IntegerCache类中的Integer数组cache中取,不在该范围则直接新建一个Integer对象,这样也就解释了上面的输出结果,127在该范围内,通过valueOf获取的是数组中的同一个对象,而128不在该范围内,新建了两个对象

2、当将Integer类型的值转换为int类型时,则会调用Integer的intValue方法,验证代码如下

public class IntegerTest {
    public static void main(String[] args) {
        Integer i1 = 1 ;
        int i2 = i1 ;
    }
}

反编译后的结果

题外话:Integer.valueOf()、Integer.intValue()分别对应“自动装箱”、“自动拆箱”

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值