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

  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() 返回一个对象的哈希值。
    • 哈希值是一个 int 类型的数,由对象中一般不变的属性映射得来,用于快速对对象进行区分、分组等
    • 一个对象的哈希值不能改变,相同对象的哈希值必须一样,反之不一定
  9. hashCode() 方法和 equals() 方法的关系是?
    答:

    • 二者联系密切,对两个对象,如果 equals() 方法返回 true,则 hashCode 也必须一样。反之不要求equals() 方法返回 false 时,hashCode 可以一样,也可以不一样,但应该尽量不一样。
    • hashCode() 的默认实现一般是将对象的内存地址转换为整数,子类如果重写了 equals() 方法,也必须重写 hashCode() 方法。之所以有这个规定,是因为 Java API 中很多类依赖于这个行为,尤其是容器中的一些类
  10. 对于 BooleanhashCode() 代码为:

    1
    2
    3
    public int hashCode() {
    return value ? 1231 : 1237;
    }

    根据基础类型值返回了两个不同的数,为什么选这两个值?

    答:因为这两个数是质数,质数用于哈希时比较好,不容易冲突

  11. Comparable 接口代码时怎样的?包装类是怎样应用的?
    答:

    • Comparable 接口代码:

      1
      2
      3
      public interface Comparable<T> {
      public int compareTo(T o);
      }
* **每个包装类都实现了 `Comparable` 接口**,接口只有一个方法:`compareTo(T o)`,表示当前对象与参数对象进行比较,在**小于、等于、大于参数时,应分别返回 -1、0、1**。
* 各个包装类的实现基本都是根据基本类型值进行比较。
* **对于 `Boolean`,`false` 小于 `true`**。
* 对于 `Float` 和 `Double`,存在和 `equals()` 方法一样的问题,0.01 和 0.1 * 0.1 相比的结果并不为 0。
  1. 包装类中常用常量有哪些?
    答:包装类中除了定义静态方法和实例方法外,还定义了一些静态变量。

    • Boolean 来说,有:

      1
      2
      public static final Boolean TRUE = new Boolean(true);
      public static final Boolean FALSE = new Boolean(false);
* 所有数值类型都定义了 **`MAX_VALUE`** 和 **`MIN_VALUE`**,表示能表示的最大值和最小值。比如,对 `Integer`,有:

    
1
2
public static final int MAX_VALUE = 0x7fffffff;
public static final int MIN_VALUE = 0x80000000;
* `Float` 和 `Double` 还定义了一些**特殊数值**,比如**正无穷、负无穷、非数值**。比如,对 `Double`,有:
1
2
3
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; // 非数值
  1. 怎样理解 Number 类?
    答:

    • Number 类是一个抽象类
    • Number 类是 6 种数值类型包装类的父类
  2. 怎样理解包装类的不可变性?
    答:

    • 包装类都是不可变类
    • 所谓不可变:指实例对象一旦创建,就没有办法修改了
  3. 包装类的不可变性是怎样实现的?
    答:是通过下面三种方式强制实现的:

    • 所有包装类都声明为了 final,不能被继承。
    • 内部基本类型值是私有的,且声明为了 final
    • 没有定义 setter 方法。
  4. 为什么要把包装类定义成不可变类?
    答:不可变使得程序更为简单安全,因为不用操心数据被意外改写的可能,可以安全地共享数据,尤其是在多线程的环境下。

  5. 怎样理解 Integer 类的位翻转二进制操作?
    答:

    • 位翻转:将 int 当做二进制,左边的位与右边的位进行互换
    • Integer 有两个静态方法:

      1
      2
      public static int reverse(int i); // 按位进行互换
      public static int reverseBytes(int i); // 按字节进行互换
    • reverseBytes(int i) 方法内部实现:通过左移、右移、与操作、或操作,达到字节翻转的目的。

  6. 怎样理解 reverse(int i) 方法源码?
    答:

    • 代码第一行是一个注释:// HD,Figure 7-1。HD 表示的是一本书,书名为《Hacker’s Delight》,中文版为《算法心得:高效算法的奥秘》,HD 是这本书的缩写,Figure 7-1 是书中的图 7-1,reverse(int i) 的代码就是复制了这本书中图 7-1 的代码。
    • 高效实现位翻转的基本思路:首先交换相邻的单一位,然后以两位为一组,再交换相邻的位,接着是 4 位一组、然后是 8 位、16位,16位之后就完成了。这个思路不仅适用于二进制,而且适用于十进制。
  7. reverse(int i) 代码为什么要写得这么晦涩?或者说不能用更容易理解的方式写吗?比如,实现翻转,一种常见的思路是:第一个和最后一个交换,第二个和倒数第二个交换,直到中间两个交换完成。
    答:

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

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

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

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