深入Java基本数据类型

深入Java基本数据类型



1. 数据类型分类

两类数据类型

  • 基本数据类型(8种)
  • 引用数据类型(包括数组和String)

1.1 基本数据类型

8种基本数据类型,4类,布尔型和字符型是无符号类型,其余都是有符号类型

  • 布尔型
    • boolean:8bit
  • 整形
    • byte:8bit
    • short:16bit
    • int:32bit
    • long:64bit
  • 浮点型
    • float:32bit
    • double:64bit
  • 字符型
    • char:16bit,存储Unicode码,‘\u0000’

1.2 基本数据类型和引用数据类型的区别

  • 概念方面
    • 基本类型:变量名指向具体的数值,数值就存储在栈空间
    • 引用类型:变量名是指向该数据对象的内存地址的指针
  • 分配内存方面
    • 基本类型:变量声明后直接分配内存空间
    • 应用类型:变量声明不会分配内存(所以会空指针异常,只有new关键字出现才会给对象分配内存)
  • 使用方面
    • 基本类型:使用时直接赋值,判断时用==
    • 应用类型:可以赋null,判断内容相等用equals方法
public class TestDataType {
    public static void main(String[] args) {
        int int1 = 100;
        int int2 = int1;
        int1 = 500;
        System.out.println(int1);//500
        System.out.println(int2);//100 说明值传递

        int[] arr1 = new int[]{1,2,3,4,5};
        int[] arr2;
        arr2 = arr1;
        arr1[2] = 8;
        System.out.println(arr1[2]);//8
        System.out.println(arr2[2]);//8

        Person ricardo = new Person("Ricardo", 21);
        Person ricardofake = ricardo;
        ricardo.setName("Jia");
        ricardo.setAge(22);
        System.out.println(ricardo);//Person{name='Jia', age=22}
        System.out.println(ricardofake);//Person{name='Jia', age=22}
    }
}

2. 数据转换

两种转换方式

  • 隐式转换,也叫自动转换
  • 显式转换,也叫强制转换

2.1 隐式转换

  • 精度小数据的转精度大的数据,比如2->2.00000,不损失精度
  • 大转小,看情况损失精度float2.5转int就是2

主要看值的范围大小,不是指的所占字节

  • 整型类型和浮点类型进行计算后变成浮点型,即使是long和float运算,也是转成float

2.2 显式转换

也叫强制转换,用()

3. 装箱和拆箱

  • 装箱:比如int转Integer,调用包装类的valueOf方法
  • 拆箱:比如Integer转int,调用包装类的xxxValue方法实现

3.1 自动装箱和自动拆箱

这里注意:因为自动装箱会隐式的创建对象,如果在一个循环体中,会创建无用的中间对象,会增加GC压力

自动装箱和拆箱的设计依赖于享元模式的设计模式

3.2 面试中相关的问题

如下代码输出结果是什么

Integer int1 = 100;
Integer int2 = 100;
Integer int3 = 500;
Integer int4 = 500;
System.out.println(int1 == int2); // true
System.out.println(int3 == int4); // false

出现这种情况说明int1和int2是同一个对象,而int3和int4并不是同一个对象

看看源码

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

看来只要是在[low, high]范围之中,就会返回同一个对象,如果不在就new一个

那我们来看看这个IntegerCache

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() {}
}

low是确定的就是-128,但是high取决于integerCacheHighPropValue这个参数,但同样不能超过int的最大值,high默认是127,再看看官方注释

Cache to support the object identity semantics of autoboxing for values between -128 and 127 (inclusive) as required by JLS. The cache is initialized on first usage. The size of the cache may be controlled by the -XX:AutoBoxCacheMax=<size> option. During VM initialization, java.lang.Integer.IntegerCache.high property may be set and saved in the private system properties in the sun.misc.VM class.

说明这个integerCacheHighPropValue变量可以在虚拟机启动时指定-XX:AutoBoxCacheMax=<size>来设置

确定了low和high之后,就创建了一个cache数组,里面放着已经初始化好的Integer类型,所以我们在工作中如果常用到的数据范围比127大,就可以通过自己指定,能减少创建对象的时间,提升性能。

同理,同为整型的Byte,Short,Long都有如此策略,但这三个不支持动态指定high,只有[-128,127],Double,Float是没有cache的,因为浮点数太多了!


再来看看Boolean

Boolean b1 = false;
Boolean b2 = false;
Boolean b3 = true;
Boolean b4 = true;
System.out.println(b1 == b2);// true
System.out.println(b3 == b4);// true

直接看源码

public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
// valueOf
public static Boolean valueOf(boolean b) {  return (b ? TRUE : FALSE);  }

static和final,决定了拿到的永远都是这两个对象,不过想想也确实,创建那么多对象太浪费,根本没用


问:Integer i = new Integer(xxx)和Integer i = xxx有什么区别

  • 前者不会触发自动装箱的过程,后者会触发
  • 后者的执行效率和资源占用要优于前者,因为用的都是cache数组中的数据,但前者又开辟新的内存,当然这也不是绝对的

再看这段代码

Integer a = 1;
Integer b = 2;
Integer c = 3;
Long d = 3L;
Long e = 2L;
System.out.println(c == (a + b)); // true
System.out.println(c.equals(a + b)); // true
System.out.println(d == (a + b)); // true
System.out.println(d.equals(a + b)); // false
System.out.println(d.equals(a + e)); // true
  • ==运算符,如果两侧都是引用类型就直接比较地址,如果一侧是表达式,则比较的是数值,也就是会触发自动拆箱
  • 对于包装器类型,equals()方法先进行自动拆箱,再进行自动装箱,不会进行类型转换
  • 第6行:包含算术运算,触发自动拆箱,调用intValue方法,都是基本数据类型,比较的是数值
  • 第7行:先进行自动拆箱,a和b分别调用intValue方法,得到最终的结果后进行自动装箱,调用valueOf方法,在进行equals比较,调用equals,再拆箱,判断是否是同一对象,一致则进行数值比较
  • 第8行:两边都转成基本数据类型,都指向数值为3的堆
  • 第9行:equals不会进行类型转换
  • 第10行:a + e被转成了long类型,所以装箱也是调用的Long.valueOf方法,参考下面的源码
// Long类型的equals方法
public boolean equals(Object obj) {
    if (obj instanceof Long) { //第9行判断为false,第10行判断为true
        return value == ((Long)obj).longValue();
    }
    return false;
}

第8行参考这个

long l1 = 10L;
int i3 = 10;
System.out.println(l1 == i3); // true

4. 判等条件

4.1 包装类的判等

两种判等方法

  • ==运算符
  • equals方法

对基本类型,比如int,long,只能使用==,比较的是值

对引用类型,比如Integer,Long,判断内容是否相等需要使用equals方法,==判断的是指针,也就是内存地址

4.2 String的判等

先看一个案例

String a = "ricardo";
String b = "ricardo";
System.out.println(a == b); // true

当以这种形式创建字符串对象时,JVM会先对这个字符串进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,就返回这个引用,否则就创建这个引用放入常量池,再返回,这种机制就是字符串驻留或池化。

继续看

String c = new String("Jia");
String d = new String("Jia");
System.out.println(c == d); // false

两个new关键字,就是开辟了两个内存空间,用==进行比较,拿到的指针当然不同

再看

String e = new String("Jia").intern();
String f = new String("Jia").intern();
System.out.println(e == f); // true

调用intern方法,也可以让字符串驻留,加入到了常量池,但在业务代码中滥用可能会造成性能问题,没事别轻易用这个,如果要用一定要控制主流的字符串数量,并留意常量表的各项指标

4.3 实现equals方法

Object类的equals实现就是比较对象的引用

public boolean equals(Object obj) {
    return (this == obj);
}

之所以Integer和String能通过equals实现内容判等,因为他们都重写了equals方法

对于自定义类型,如果不重写equals就默认使用Object类的比较方式

实现一个好的equals应该注意的点

  • 考虑到性能,可以先进行指针判定,如果地址都是同一个,直接返回true
  • 需要对另一方进行判空,空对象和自身比较,直接返回false
  • 判断两个对象的类型,如果类型都不一样,直接返回false
  • 确保类型相同的情况下,进行强制类型转换,然后对字段逐一判定

看个例子

有一个自定义的Point类

public class Point {
    private int x;
    private int y;
}

我们来为他重写equals方法

@Override
public boolean equals(Object o) {
    if (this == o) return true; // 判断指针是否相同
    if (o == null || getClass() != o.getClass()) return false;// 判断比较对象是否为null和判断类型是否相同

    Point point = (Point) o;// 进行强制类型转换

    if (x != point.x) return false;// 看业务需要对需要判等的属性进行判断
    return y == point.y;
}

4.4 hashCode和equals要配对实现

4.4.1 Why❓

当我们重写了equals方法,完成了对内容的等值匹配。但是如果要在一个集合中不重复的放数据,里面有了一百万个数据,我们再插入一个数据,难道要一个一个调用equals方法去匹配吗。当然不行。

所以我们有了哈希表,先通过内容对其进行hash运算,如果一致了,再进行equals匹配内容。

如果equals相同,那么hashCode一定相同;反过来,如果hashCode相同,equals不一定相同。

当然,hashCode和equals也并不是强制要配对的,如果说不需要将你的类型放入Set,Map这种不能重复的集合,那么其实也不需要重写hashCode方法,但是如果你的类型要放入Set,要保证你的类型唯一,那么就必须重写hashCode方法。

看个栗子🌰

我们只重写equals方法

public class Point {
    private int x;
    private int y;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Point point = (Point) o;

        if (x != point.x) return false;
        return y == point.y;
    }
}
public static void main(String[] args) {
    Point point1 = new Point(1, 2);
    Point point2 = new Point(1, 2);
    System.out.println(point1.equals(point2)); // true
}

这样用雀湿没问题

那我们看看放入Set里看看

public static void main(String[] args) {
    Point point1 = new Point(1, 2);
    Point point2 = new Point(2, 6);
	Point point3 = new Point(1, 2);
    Set<Point> points = new HashSet<>();
    points.add(point1);
    points.add(point2);
    points.add(point3);
    for (Point point : points) {
        System.out.println(point);
    }
}
-------------------------------
Point{x=1, y=2}
Point{x=2, y=6}
Point{x=1, y=2}

不重复的集合中出现了重复的元素

那么我们加上hashCode方法,重新执行

@Override
public int hashCode() {
    int result = x;
    result = 31 * result + y;
    return result;
}

---------------------
Point{x=1, y=2}
Point{x=2, y=6}

不重复了,达到了我们的目的,由此就能看出为何要配对使用了

看看Object类中对equals方法的注释

Note that it is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes.

意思就是说已经形成了默认的约定,只要重写equals方法就必须重写hashCode方法

4.4.2 道理我都懂,所以为什么是31?

先说结论,为了性能

在《Effective Java》第二版中42页有解释为什么是31

image-20210904162340396

选择 31 是因为它是奇素数。如果是偶数,乘法运算就会溢出,信息就会丢失,因为乘法运算等同于移位。使用素数的好处不太明显,但它是传统用法。31 有一个很好的特性,可以用移位和减法来代替乘法,从而在某些体系结构上获得更好的性能:31 * i == (i <<5) – i。现代虚拟机自动进行这种优化。

31只占5bit,不会因为一次的移位就会变得很大导致数据溢出,同时31也是素数,只能被1和31本身整除,在进行乘法计算时能很快得出结果

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值