0%

Java 常用基础类(一):包装类

1. 包装类是什么

  • Java 有 8 种基本数据类型,每种基本类型都有一个对应的包装类
  • 包装类是一个类,内部有一个实例变量,保存着对应的基本类型的值,这个类一般还有一些静态方法、静态变量和实例方法,以方便对数据进行操作。
  • 除了 IntegerCharacter,其他包装类的类名称与基本类型基本一样,只是首字母大写

2. 包装类有什么用

  • Java 中很多代码(比如容器类)只能操作对象,为了能操作基本数据类型,需要使用其对应的包装类。
  • 包装类提供了很多有用的方法,可以方便对数据操作。

3. 包装类与基本类型怎样互相转换

  • 每种包装类都有一个静态方法 valueOf(),接收基本类型,返回引用类型。
  • 也都有一个实例方法 xxxValue(),返回对应的基本类型。

4. 怎样理解自动装箱和自动拆箱

  • 装箱:将基本类型转换为包装类的过程。
  • 拆箱:将包装类型转换为基本类型的过程。
  • 装箱和拆箱写起来比较繁琐,Java 5 以后引入了自动装箱和自动拆箱技术,可以直接将基本类型赋值给引用类型,反之亦可。
  • 自动装箱/自动拆箱是 Java 编译器提供的能力。
  • 每种包装类也都有构造方法,可以通过 new 创建(不推荐)。

5. 创建包装类对象,应该用静态的 valueOf() 方法还是使用 new 呢

  • 一般建议使用静态 valueOf() 方法。
  • new 每次都会创建一个新对象,而除了 FloatDouble 外的其他包装类,都会缓存包装类对象,减少需要创建对象的次数,节省空间、提升性能。
  • 实际上,从 Java 9 开始,这些构造方法已经被标记为过时了,推荐使用静态的 valueOf() 方法

6. 所有包装类都重写了 Object 类的哪些方法

 boolean equals(Object obj)int hashCode()String toString()

7. 怎样理解 equals() 方法

  • equals() 用于判断当前对象和参数传入的对象是否相同。
  • Object 类的默认实现是比较地址,对于两个变量,只有这两个变量指向同一个对象时,equals() 才返回 true,它和比较运算符 == 的结果是一样的。
  • equals() 应该反映的是对象间的逻辑相等关系,所以 Object 类的默认实现一般是不合适的,子类经常需要重写该实现以实现自己期望的相等判断逻辑。
  • 所有包装类都重写了该实现,实际比较用的是其包装类的基本类型值(看源码)。

8. 怎样理解 hashCode() 方法

  • hashCode() 返回一个对象的哈希值,在 Object 类中用 native 修饰是一个本地方法、没有给出具体的实现。
  • 哈希值是一个 int 类型的数,由对象中一般不变的属性映射得来,用于快速对对象进行区分、分组等
  • 一个对象的哈希值不能改变,相同对象的哈希值必须一样,反之不一定

9. hashCode() 方法和 equals() 方法的关系是

  • 二者联系密切,对两个对象,如果 equals() 方法返回 true,则 hashCode 也必须一样,反之不要求。equals() 方法返回 false 时,hashCode 可以一样,也可以不一样,但应该尽量不一样。
  • hashCode() 的默认实现一般是将对象的内存地址转换为整数,子类如果重写了 equals() 方法,也必须重写 hashCode() 方法(否则和依赖与 hash 的集合类,比如 HashMapHashSetHashTable 等,一起使用时就会出错)。
  • 之所以有这个规定,是因为 Java API 中很多类依赖于这个行为,尤其是容器中的一些类
  • 参考:为什么要重写 hashcode 和 equals 方法

10. 对于包装类 Boolean,其 hashCode() 代码为:public int hashCode() { return value ? 1231 : 1237; } 根据基础类型值返回了两个不同的数,为什么选这两个值

 因为这两个数是质数(只能被 1 和自身整除),质数用于哈希时比较好,不容易冲突

11. Comparable 接口代码是怎样的,包装类是怎样应用的

  • Comparable 接口代码:

    1
    2
    3
    public interface Comparable<T> {
    public int compareTo(T o);
    }
  • 每个包装类都实现了 Comparable 接口,接口只有一个方法:compareTo(T o),表示当前对象与参数对象进行比较,在小于、等于、大于参数时,应分别返回 -1、0、1

  • 各个包装类的实现基本都是根据基本类型值进行比较。

  • 对于 Booleanfalse 小于 true

  • 对于 FloatDouble,存在和 equals() 方法一样的问题,0.01 和 0.1 * 0.1 相比的结果并不为 0(因为计算机本身就不能精确计算很多数,这是二进制的局限,但肯定比十进制优越得多)。

12. 包装类中常用常量有哪些

  • 包装类中除了定义静态方法和实例方法外,还定义了一些静态变量,对 Boolean 来说,有:
    • public static final Boolean TRUE = new Boolean(true);
    • public static final Boolean FALSE = new Boolean(false);
  • 所有数值类型都定义了 MAX_VALUEMIN_VALUE,表示能表示的最大值和最小值。比如,对 Integer,有:
    • public static final int MAX_VALUE = 0x7fffffff;
    • public static final int MIN_VALUE = 0x80000000;
  • FloatDouble 还定义了一些特殊数值,比如正无穷负无穷非数值。比如,对于 Double,有:
    • public static final double POSITIVE_INFINITY = 1.0 / 0.0; //正无穷
    • public static final double NEGATIVE_INFINITY = -1.0 / 0.0; //负无穷
    • public static final double NaN = 0.0 d / 0.0; //非数值(0 除以 0,或者 0 乘以无穷大)

13. 怎样理解 Number 类

  • Number 类是一个抽象类
  • Number 类是 6 种数值类型包装类的父类

14. 怎样理解包装类的不可变性

  • 包装类都是不可变类
  • 所谓不可变:指实例对象一旦创建,就没有办法修改了

15. 包装类的不可变性是怎样实现的

  • 是通过下面三种方式强制实现的:
    1. 所有包装类都声明为了 final,不能被继承。
    2. 内部基本类型值是私有的,且声明为了 final
    3. 没有定义 setter 方法。

16. 为什么要把包装类定义成不可变类

  • 不可变使得程序更为简单安全,因为不用操心数据被意外改写的可能.
  • 可以安全地共享数据,尤其是在多线程的环境下。

17. 怎样理解 Integer 类的位翻转二进制操作

  • 位翻转:将 int 当做二进制,左边的位与右边的位进行互换
  • Integer 有两个静态方法:
    • public static int reverse(int i); //按位进行互换
    • public static int reverseBytes(int i); //按字节进行互换
  • reverseBytes(int i) 方法内部实现:通过左移、右移、与操作、或操作,达到字节翻转的目的。

18. 怎样理解 reverse(int i) 方法源码

  • 代码第一行是一个注释:// HD,Figure 7-1
    • HD 表示的是一本书,书名为《Hacker’s Delight》,中文版为《算法心得:高效算法的奥秘》(这翻译的中文名真不geek)
    • HD 是这本书的缩写,Figure 7-1 是书中的图 7-1,reverse(int i) 的代码就是复制了这本书中图 7-1 的代码。
  • 高效实现位翻转的基本思路:首先交换相邻的单一位,然后以两位为一组,再交换相邻的位,接着是 4 位一组、然后是 8 位、16位,16位之后就完成了。这个思路不仅适用于二进制,而且适用于十进制。

19. reverse(int i) 代码为什么要写得这么晦涩,或者说不能用更容易理解的方式写吗。比如,实现翻转,一种常见的思路是:第一个和最后一个交换,第二个和倒数第二个交换,直到中间两个交换完成。

  • 如果数据不是二进制,这个思路是好的,但对于二进制,这个思路实现的效率比较低。
  • CPU 指令并不能高效地操作单个位,它操作的最小数据单位一般是 32 位(32位机器)。另外,CPU 可以高效地实现移位和逻辑运算,但实现加、减、乘、除运算则比较慢
  • reverse(int i) 是在充分利用 CPU 的这些特性,并行高效地进行相邻位的交换。也可以通过其他更容易理解的方式实现相同功能,但很难比这个代码更高效。

20. 怎样理解 valueOf() 方法的实现

  • 方法内部使用了 IntegerCache,这是一个私有静态内部类。
  • IntegerCache 表示 Integer 缓存,其中的 cache 变量是一个静态的 Integer 数组,在静态初始化代码块中被初始化。默认情况下,保存了 -128 ~ 127 共 256 个整数对应的 Integer 对象
  • valueOf() 代码中,如果数值位于被缓存的范围,即默认 -128 ~ 127,则直接从 IntegerCache 中获取已预先创建的 Integer 对象。因为这个范围的数字使用频率较高,只有不在缓存范围时,才通过 new 创建对象
  • 通过共享常用对象,可以节省内存空间,由于 Integer 是不可变的,所以缓存的对象可以安全地被共享BooleanByteShortLongCharacter 都有类似的实现。
  • 这种共享常用对象的思路,是一种常见的设计模式,叫享元模式。英文叫 Flyweight,即共享的轻量级元素

21. 怎样理解 Unicode

  • Unicode 给世界上每个字符都分配了一个编号,编号范围为:0x000000 ~ 0x10FFFF。
    • 常用字符集(BMP: Basic Multilingual Plane):编号范围在 0x0000 ~ 0xFFFF 的字符。
    • 增补字符(supplementary character):编号范围在 0x10000 ~ 0x10FFFF 的字符。
    • Unicode 主要规定了编号,但没有规定如何把编号映射为二进制。
  • UTF-16 是一种编码方式,或者叫映射方式,它将编号映射为 2 个或 4 个字节。对 BMP 字符,它直接用两个字节表示,对于增补字符,使用 4 个字符表示。
    • 前两个字节叫高代理项(high surrogate),范围为 0xD800 ~ 0xDBFF;后两个字节叫低代理项(low surrogate),范围为 0xDC00 ~ 0xDFFF。
    • UTF-16 定义了一个公式,可以将编号与 4 字节表示进行相互转换。
  • Java 内部采用 UTF-16 编码,char 表示一个字符,但只能表示 BMP 中的字符,对于增补字符,需要使用两个 char 表示,一个表示高代理项,一个表示低代理项。
    • 使用 int 可以表示任意一个 Unicode 字符,低 21 位表示 Unicode 编号,高 11 位设为 0。
    • 整数编号在 Unicode 中一般称为代码点(code point),表示一个 Unicode 字符;与之相对,还有一个词代码单元(code unit)表示一个 char
  • Unicode 在给每个字符分配一个编号之外,还分配了一些属性Character 类封装了对 Unicode 字符属性的检查和操作。Unicode 给每个字符分配了一个类型。
    • 需要注意的是,不光字符 '0''1'、……、'9' 是数字,中文全角字符的 0 ~ 9 也是数字。
    • 更常用的检查空格的方法:pubilc static boolean isWhitespace(int codepoint),不仅能匹配空格字符本身,还能匹配实际产生空格效果的字符,如 Tab 控制键'\t''\n'、全角空格' ' 和 半角空格' ' 的返回值都为 true

22. 怎样理解 Java 标识符

  • Java 标识符:是 Java 中的变量名、方法名、类名等。
  • 字母(Alphabetic)、美元符号($)、下划线(_)可以作为 Java 标识符的第一个字符,但数字字符不可以。
  • Java 标识符的中间字符可以包含数字

23. Character 类的作用

  • Character 有很多静态方法,封装了 Unicode 字符级别(而非 char 级别)的各种操作,是 Java 文本处理的基础
  • 通过将字符处理的细节交给 Character 类,其他类就可以在更高的层次上处理文本了。

24. 8 种基本类型的包装类和常量池

 参考:可能是把 Java 内存区域讲的最清楚的一篇文章

-------------------- 本文结束感谢您的阅读 --------------------