通用类型系统是构建类型的基础。这里使用的“通用类型系统”的概念来源于 C#,这里将其引入到 Java 学习中。
调查一番发现,Java 的类型系统并不建立在 Object 类之上,这一点与 C# 不同,而与 C 语言类似。因此,放弃对 C# 的类比,转而类比 C 语言。
数据类型层次划分
Java 将数据类型共分为三类:基本类型,引用类型和空类型(null)。数据类型划分概览图:
基本类型
Java为基本类型提供语言级别的支持,即基本类型在Java语言中预定义,并使用相应的保留关键字表示。基本类型是单个值,而不是复杂的对象,基本类型不是面向对象的,主要出于效率方面的考虑。可以直接使用这些基本类型,也可以使用基本类型构造数组或者其他自定义类型。
基本类型按照是否为数字,可以分为数字类型和布尔类型。其中数字类型又可细分为三类:
(1)整型: byte, short, int(4个字节), long
(2)浮点型: float(4个字节), double
(4)字符型: char(16位,2个字节)
另外,布尔类型使用关键字boolean,且它只有两个值:true 和 false。
对于基本数据类型,需要注意:
(1) JVM 可以保证各个类型的长度固定。为屏蔽操作系统的差异,JVM实现了对基本数据类型长度的统一。
(2) 整型和浮点型使用不同的方式存储和表示数据。对于整型(有符号),使用补码的形式存储数据,且最高有效位为符号位(0表示正数,1表示负数)。对于浮点型,根据IEEE 754标准,将任意一个二进制浮点数V用以下的形式表示:
V = ( −1)^S * M ∗ 2^E
其中,表示符号位,当S=0,V为正数;当S=1,V为负数。M表示有效数字,大于等于1,小于2。E表示指数。
举例来说,十进制的-5.0对应的二进制是-101.0,相当于-1.01×2^2(二进制的科学表示法)。进一步可知,S=1,M=1.01,E=2。在存储浮点数据时,真实存储的是S、M和E。
IEEE 754规定,对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。对应图表如下:
对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
对于浮点数,整数部分和小数部分单独表示。对于,十进制的整数部分可以完全由二进制表示,但是对于小数部分,部分十进制小数,无法完全由二进制表示,只能近似表示。小数对于十进制来说,可以想象成把1拆成10份,而对于二进制来说,就是把1拆成2份。
如十进制的0.5可以精确地用二进制的0.1表示,但是如0.9却不能精确表示,只能根据精度进行表示,如精度为3,0.111(二进制)<0.9(十进制)<0.1111(二进制)
所以,在对浮点数进行相等比较时,使用==并不能得到准确的结果,而是保证差值在指定范围内或使用BigDecimal封装(内置精度)。更多说明参考《阿里巴巴Java开发手册》
// 反例:
float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
if (a == b) {
// 预期进入此代码快,执行其它业务逻辑
// 但事实上 a==b 的结果为 false
}
Float x = Float.valueOf(a);
Float y = Float.valueOf(b);
if (x.equals(y)) {
// 预期进入此代码快,执行其它业务逻辑
// 但事实上 equals 的结果为 false
}
// 正例:
// (1) 指定一个误差范围,两个浮点数的差值在此范围之内,则认为是相等的。
float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
float diff = 1e-6f;
if (Math.abs(a - b) < diff) {
System.out.println("true");
}
// (2) 使用 BigDecimal 来定义值,再进行浮点数的运算操作。
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);
if (x.equals(y)) {
System.out.println("true");
}
(3) char类型占用2个字节。Java中char表示Unicode字符,但是兼容ASCII字符(0-127位表示字符为ASCII字符)注意,char是无符号数据类型。因此,char变量不能为负值。字符数据类型的范围为0到65535,这与Unicode-16字符集的范围相同。
(4) Java中无string基本类型,仅有String类。
(5) Java并不是严格的面向对象语言就是因为存在8中基本类型数据(int、char、double、boolean等),它们并不是类对象。
引用类型
引用数据类型建立在基本数据类型基础上,包括数类(Class)、接口(Interface)、枚举(Enum)和数组(Array)。引用数据类型是由用户自定义,用来限制其他数据的类型。另外,Java 语言不支持 C++ 语言中的指针类型、结构类型、联合类型。
String
在Java中,String类型使用频度极高,String不是值类型(基本数据类型),但却表现出值类型特性。
Java重写了操作符,从而保证可以使用进行String实例的比较。Java重写了+操作符,从而保证可以使用+进行字符串的拼接。
String中equals和==
String对equals进行重写,比较值是否相等。实现如下:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}
因为String重写的equals方法,所以下述代码返回为true。
String str1 = new String("123");
String str2 = new String("123");
// true
str1.equals(str2);
由于==运算符对于“引用类型”仅比较是否指向同一片内存,所以new的两个String实例不相等。示例代码如下:
String str1 = new String("123");
String str2 = new String("123");
// false
str1 == str2;
String包装器支持字面量赋值,当使用形如:stringInstance="xxx"方式创建字符串时,程序首先会在字符串常量池中寻找相同值的对象,如果池中无相同值的对象,则在池中添加一个值。所以以下代码值相等:
String str1 = "123";
// true
str1 == "123";
但也应注意,如果使用new的方式创建字符串示例,则使用==会认为两边不相等。示例代码如下:
String str1 = new String("123");
// false
str1 == "123";
所以,在对字符串进行相等比较时,尽量使用equals而不是使用==。
字符串拼接
String 对象是不可变对象, 因此每次对 String 改变,其实都等同于生成一个新的 String 对象,然后将指针指向新的 String 对象。所以对经常更变内容的字符串最好不要用 String 表示。
为简化字符串拼接,Java重写+操作符,使其支持字符串拼接。在String + 拼接的底层,是StringBuilder实现的,整个过程是StringBuilder append之后toString。(Java9 改成了invokedynamic,StringConcatFactory.makeConcatWithConstants)
为验证String + 操作符的底层实现,编写如下Java代码:
public class Main {
public static void main(String[] args) {
String str1 = "123";
String str2 = "456";
String str3 = str1 + str2;
}
}
使用javac命令生成字节码后,再使用javap命令可获得反编译后代码:
// Java 12
public class io.github.courage007.Main {
public io.github.courage007.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String 123
2: astore_1
3: ldc #3 // String 456
5: astore_2
6: aload_1
7: aload_2
8: invokedynamic #4, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
13: astore_3
14: return
}
枚举类型
枚举类型是Java 5中新增的类型。Java在类型划分时,将枚举类型作为独立的数据类型。本质上,枚举就是特殊的类,只是域成员均为常量,且构造方法被默认强制为私有。
直接使用枚举类型,示例如下:
// (1) 定义枚举类型
pubic enum ColorEnum {
RED,
GREEN,
YELLOW,
BLUE
}
// (2) 使用枚举值
public class Main {
public static void main(String[] args) {
System.out.println(ColorEnum.RED;);
}
}
在实际应用中,有时需要使用枚举值表示的编码,这时需要进一步补充枚举的定义。示例如下:
// (1) 定义枚举类型并添加自定义构造函数
public enum ColorEnum {
RED(1,"红色"),
GREEN(2, "绿色"),
YELLOW(3,"黄色"),
BLUE(4, "蓝色"); // 注意,最后一个枚举值使用分号结尾
// 编号
private int code;
// 名称(描述)
private String name;
/**
* 私有构造,防止被外部调用
*/
private ColorEnum(int code, String name) {
this.code = code;
this.name = name;
}
public int getCode() {
return code;
}
public String getName() {
return name;
}
}
// (2) 使用枚举值
public class Main {
public static void main(String[] args) {
System.out.println(ColorEnum.RED.getCode());
System.out.println(ColorEnum.RED.getName());
}
}
更多Enum相关话题参考链接。
空类型
空类型是一种特殊的type, 唯一取值就是null。无法声明一个变量为 null 类型,但可将 null 赋值给任意的引用类型或者转化成任意的引用类型。在实践中,一般把 null 当做字面值(literal),这个字面值可以是任意的引用类型。
基本数据类型和引用类型对比
数据类型高级特性
类型转换
自动类型转换与强制类型转换
值类型的装箱与拆箱
值类型的装箱与拆箱经常在以下场景使用:
(1)调用一个含类型为Object的参数时,如果需要将一个值类型(如Int32)传入时,需要装箱;
(2)对于一个非泛型的容器,将元素类型定义为Object时,如果要将值类型数据加入容器中,需要装箱。
装箱: 栈 -> 堆
拆箱: 堆 -> 栈
内存分配
基本数据类型内存分配和引用类型内存分配。
所有类型都从Object派生(单根继承)
单根继承的一大特点是所有的类都继承自一个共同的基类:Object。Object类提供的非私有方法有:
1.clone方法
保护方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。
2.getClass方法
final方法,获得运行时类型。
3.toString方法
该方法用得比较多,一般子类都有覆盖。
4.finalize方法
该方法用于释放资源。
5.equals方法
该方法是非常重要的一个方法。一般equals和==是不一样的,但是在Object中两者是一样的。子类一般都要重写这个方法。
6.hashCode方法
该方法用于哈希查找,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。
一般必须满足obj1.equals(obj2)==true。可以推出obj1.hashCode() == obj2.hashCode(),但是hashCode相等不一定就满足equals。不过为了提高效率,应该尽量使上面两个条件接近等价。
7.wait方法
wait方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。调用该方法后当前线程进入睡眠状态,直到以下事件发生:
(1)其他线程调用了该对象的notify方法;
(2)其他线程调用了该对象的notifyAll方法;
(3)其他线程调用了interrupt中断该线程;
(4)时间间隔到了。
此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。
8.notify方法
该方法唤醒在该对象上等待的某个线程。
9.notifyAll方法
该方法唤醒在该对象上等待的所有线程。