基本类型和包装类型
基本类型
Java 中有 8 种基本数据类型,分别为:
- 6 种数字类型:
- 4 种整数型:byte、short、int、long
- 2 种浮点型:float、double
- 1 种字符类型:char
- 1 种布尔型:boolean
包装类型
如上8种基本数据类型都有对应的包装类型,这些包装类型都是不可变的类,它们提供了基本数据类型与对象之间的转换(解决基本数据类型无法直接应用于需要对象的环境中,例如在集合或泛型编程中的问题),以及一些有用的方法(字符串/数值转换,比值等)。以下是基本数据类型及其对应的包装类型:
- byte 对应的包装类型是 java.lang.Byte
- short 对应的包装类型是 java.lang.Short
- int 对应的包装类型是 java.lang.Integer
- long -对应的包装类型是 java.lang.Long
- float 对应的包装类型是 java.lang.Float
- double 对应的包装类型是 java.lang.Double
- char 对应的包装类型是 java.lang.Character
- boolean 对应的包装类型是 java.lang.Boolean
包装类型的缓存机制
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
-
Byte,Short,Integer,Long 这 4 种包装类创建了数值 [-128,127] 的缓存数据
-
Character 创建了数值在 [0,127] 范围的缓存数据
-
Boolean 直接返回 True or False。
-
Float,Double 并没有实现缓存机制。
基本类型和包装类型的区别?
- 用途:除了定义一些常量和局部变量之外,我们在其他地方,比如方法参数、对象属性中很少会使用基本类型来定义变量。并且,包装类型可用于泛型,而基本类型不可以。
- 存储方式:
- 基本数据类型的局部变量存放在 Java 虚拟机的栈中的局部变量表中,基本数据类型的成员变量(未被static 修饰 ) 存放在 Java虚拟机的堆中。
- 包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。
- 占用空间:相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小。
- 默认值:成员变量包装类型不赋值就是 null ,而基本类型有默认值且不是 null。
- 比较方式:对于基本数据类型来说,
==
比较的是值。对于包装数据类型来说,==
比较的是对象的内存地址
public class Test {
// 成员变量,存放在堆中
int a = 10;
// 被 static 修饰,也存放在堆中,但属于类,不属于对象
// JDK1.7 静态变量从永久代移动了 Java 堆中
static int b = 20;
public void method() {
// 局部变量,存放在栈中
int c = 30;
static int d = 40; // 编译错误,不能在方法中使用 static 修饰局部变量
}
}
自动拆箱和装箱
- 装箱:基本类型 -> 包装类型;底层调用的是包装类的valueOf()方法
- 拆箱:包装类型 -> 基本类型;底层调用的是 xxxValue()方法
举例如下:
Integer i = 10; //装箱,等价于 Integer i = Integer.valueOf(10)
int n = i; //拆箱,等价于 int n = i.intValue();
所以如果频繁拆装箱的话,会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。
对象==
比较
public static void main(String[] args) {
Integer a = 1000;
Integer b = 1000;
Integer c = 100;
Integer d = 100;
System.out.println("a == b is " + (a == b));
System.out.println(("c == d is " + (c == d)));
}
输出:
a == b is false
c == d is true
整型对象通过使用相同的对象引用实现了缓存和重用。适用于整数值区间-128 至 +127。只适用于自动装箱。使用构造函数创建对象不适用。
浮点数与精度丢失问题(BigDecimal)
我们都知道,计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。
为此,我们引入了BigDecimal,它可以实现对浮点数的运算,且不会造成精度丢失。比如涉及到钱的业务场景,都是通过 BigDecimal 来做的。
其比较内存地址用的是Objects.equals
,比较值用的是compareTo
。
基本大部分包装类型比较值都用的是compareTo
,因为基本都实现了Comparable
接口,但是整数包装类型往往用的是equals
,因为equals
方法和原始的比较操作符 ==
的行为是一致的,而 compareTo
方法往往用于排序操作。注意这里的equals
方法指的不是Objects.equals
,而是对象a.equals(对象b)
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(c);
BigDecimal y = b.subtract(c);
System.out.println(x); /* 0.2 */
System.out.println(y); /* 0.20 */
// 比较内存地址,不是比较值
System.out.println(Objects.equals(x, y)); /* false */
// 比较值相等用相等compareTo,相等返回0
System.out.println(0 == x.compareTo(y)); /* true */
静态变量和静态方法(static修饰)
静态变量
静态变量也就是被 static 关键字修饰的变量。它可以被类的所有实例共享,这样可以节省内存。
静态变量是通过类名来访问的(除了该静态变量被 private关键字修饰的情况)。
通常情况下,静态变量会进一步被 final 关键字修饰成为常量。
静态方法
静态方法为什么不能调用非静态成员?
主要原因如下:静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
重载和重写
重载和重写的区别
重载就是同一个类的两个同名方法能够根据输入数据的不同,做出不同的处理。
重写就是当子类继承自父类的相同方法时对于内部逻辑的覆盖。
具体区别如下:
方法类型 | 范围 | 参数列表 | 返回类型 | 异常 | 访问修饰符 | 发生阶段 |
---|---|---|---|---|---|---|
重载方法 | 同一个类 | 必须修改 | 可修改 | 可修改 | 可修改 | 编译期 |
重写方法 | 子类 | 一定不能修改 | 子类方法返回值类型应比父类方法返回值类型更小或相等 | 子类抛出的异常类应比父类抛出的异常类更小或相等 | 可修改 | 运行期 |
重写的注意点
关于重写,还有两点需要注意:
- 构造方法无法被重写
- 如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
- 我们可以使用
@Override
注释在想要重写的方法上,但它只起到检查作用,发现重写后的方法并没有达到重写的要求(譬如不小心修改了参数列表等),会使代码报错从而告警。
关于被 static 修饰的方法能够被再次声明的例子如下:
class Parent {
public static void staticMethod() {
System.out.println("Parent's static method");
}
public void parentMethod() {
System.out.println("Parent's method");
}
}
class Child extends Parent {
// 子类可以声明一个与父类静态方法同名的方法,但这不是重写,它只是隐藏了父类的方法
public static void staticMethod() {
System.out.println("Child's static method");
}
// 如下才是重写
@Override
public void parentMethod() {
System.out.println("Child's method");
}
}
public class Test {
public static void main(String[] args) {
Parent parent = new Child();
// 通过子类引用调用父类的静态方法,实际走的是Child类里面的staticMethod代码
parent.staticMethod(); // 输出: Child's static method
// 通过子类引用调用父类的普通方法,实际走的是Child类里面的parentMethod代码
parent.parentMethod(); // 输出: Child's method
}
}
重载的注意点
可变长参数
什么是可变长参数?
所谓可变长参数就是允许在调用方法时传入不定长度的参数,编译后实际会被转换成一个数组。就比如下面这个方法就可以接受 0 个或者多个参数。
public static void method1(String... args) {
//......
}
另外,当可变参数和不可变参数一起时,可变参数只能作为函数的最后一个参数。
public static void method2(String arg1, String... args) {
//......
}
遇到方法重载的情况怎么办呢?会优先匹配固定参数还是可变参数的方法呢?
答案是会优先匹配固定参数的方法,因为固定参数的方法匹配度更高。如下:
public class VariableLengthArgument {
public static void printVariable(String... args) {
for (String s : args) {
System.out.println(s);
}
}
public static void printVariable(String arg1, String arg2) {
System.out.println(arg1 + arg2);
}
public static void main(String[] args) {
printVariable("a", "b");
printVariable("a", "b", "c", "d");
}
}
输出:
ab
a
b
c
d
重载遇到泛型
public class GenericTypes {
public static void method(List<String> list) {
System.out.println("invoke method(List<String> list)");
}
public static void method(List<Integer> list) {
System.out.println("invoke method(List<Integer> list)");
}
}
如上代码,有两个重载的函数,因为他们的参数类型不同,一个是List<String>
另一个是List<Integer>
,但是,这段代码是编译通不过的。因为泛型会擦除类型,将他们变成了一样的原生类型 List
,擦除动作导致这两个方法的特征签名变得一模一样。具体可见本专栏p6的泛型一节。