目录
类与对象
自定义类
对象的内存模型
“=”赋值
对象引用:this
作为常量的对象变量:final
判等操作
1. “重写(override)”基类的equals()方法
2. “重载(overload)”equals()方法
对象的构造
有参构造与无参构造
构造方法重载
初始化
类的静态成员
静态字段
静态常量
静态初始化块
静态方法
工厂方法
main 方法
静态成员的使用
枚举类型
包装类
父类Number方法
手动装箱/手动拆箱
javap反编译
缓存
1. Integer 缓存
2. 超出缓存范围
类与对象
自定义类
编写类的 “模板”两种最基本的类成员存取权限:
1. public: 存取与访问不受限制,可以随意被外界读取
2. private: 类自己的方法读取和访问,外界代码不能存取和访问
/**
* 自定义Java类的示例
*/
public class MyClass {
//公有字段
public String information = "";
//自定义公有Java实例方法
public void myMethod(String argu) {
System.out.println(argu);
}
//region "属性 = 私有字段 + get方法 + set方法
private int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
//endregion
}
class UseMyClass {
public static void main(String[] args) {
//创建类的实例
MyClass obj = new MyClass();
//通过对象变量调用类的公有方法
obj.myMethod("Hello");
//给属性赋值
obj.setValue(100);
//输出属性的当前值
System.out.println(obj.getValue());
//直接访问对象公有字段
obj.information = "Information";
//输出对象公有字段的当前值
System.out.println(obj.information);
}
}
1. 我们需要定义一个对象变量
2. 然后“创建(new)”一个对象,赋值给这个对象变量
3. 现在就可以通过这个对象变量使用对象,主要方式有:
① 直接调用类定义的公有(public)方法
② 存取类定义的公有字段
tips:在IntelliJ中编写类的代码时,使用 Alt + Ins 组合键,会弹出一个快捷菜单,其中包容许多常用的命令,可以方便地生成各种框架代码,能够极大地提升编码效率。
对象的内存模型
“引用”一个对象的变量称为“引用类型”的变量,有时又简称为“对象变量” 。
诸如 int,float 之类的变量则称为“原始数据类型”的变量 。
当声明一个对象类型的变量时,实际上并没有创建一个对象,此变量值为默认值 null 。(虽然有默认值 null ,但是在未初始化的状态下不允许使用,所以给对象变量如果不引用一个真实的对象,必须声明为null。) 下面左边的是错误操作,右边的是正确的。
tips:对象变量的特殊值null:代表一个对象变量不引用任何对象。
下面等价的两种写法:
MyClass obj = new MyClass();
MyClass obj;
obj = new MyClass();
定义一个原始数据类型的变量时,则会马上给其分配内存并初始化(如果没有指定,则为默认值,通常是0)。
public class MyClass {
private int value = 100;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
//endregion
}
如果在MyClass类中对value给定了初始化的值,那么在创建一个新的变量时(执行 MyClass obj = new MyClass(); 语句时),会自动给value初始化为100。
new这一关键字的作用,就是通知JVM为创建出来的新对象分配足够量的内存(在堆中),并自动调用此对象的一个特定方法(称为“构造方法”)对这块内存进行初始化。
“=”赋值
对象变量之间的赋值,本质上是指向对象的那个内存地址值的复制(而不是对象本身的复制)。赋值结束之后,两个对象变量引用同一个对象。
对象引用:this
this关键字是Java中的一个特殊引用变量,它持有当前对象的引用。在类的非静态方法和构造函数中,可以使用this关键字访问当前对象的成员变量和方法。简单来说,this关键字可以理解为“当前对象自身”的代称。
同一类中的方法可以相互调用,或者直接存取本类定义的字段,可看成其中隐含了一个this调用。
方法中的变量为局部变量,存储在栈中,作用范围是方法内;我们想通过构造器初始化的是成员变量,存储在堆中,作用范围是本类内部。
public class MyClass {
private int value;
public void setValue1(int value) {
this.value = value;
}
public void setValue2(int value) {
value = value;
}
public String toString() {
return "MyClass [value=" + value + "]";
}
}
class UseMyClass {
public static void main(String[] args) {
//创建类的实例
MyClass obj = new MyClass();
obj.setValue1(100);
System.out.println(obj.toString());
obj.setValue2(200);
System.out.println(obj.toString());
}
}
- 通过这个例子,你会发现 setValue2 方法没有实现赋值的功能,因为当成员变量&局部变量重名时,优先使用局部变量。关键还是看有没有局部变量,有局部变量优先使用局部变量。
所以需要使用this.属性
来表明这是一个成员变量,与局部变量以示区分。
作为常量的对象变量:final
final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)。
final MyClass obj = new MyClass();
注意:“常量”对象变量不能引用另一个对象,但可以修改对象所包容的数据,比如修改它所引用的那个对象的某个公有字段值为另一个值。
1.修饰类
当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。
在使用final修饰类的时候,要注意谨慎选择,除非这个类真的在以后不会用来继承或者出于安全的考虑,尽量不要将类设计为final类。
2.修饰方法
下面这段话摘自《Java编程思想》第四版第143页:
“使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。“
因此,如果只有在想明确禁止 该方法在子类中被覆盖的情况下才将方法设置为final的。
注:类的private方法会隐式地被指定为final方法。
3.修饰变量
修饰变量是final用得最多的地方,也是本文接下来要重点阐述的内容。首先了解一下final变量的基本语法:
对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
注意:“常量”对象变量不能引用另一个对象,但可以修改对象所包容的数据,比如修改它所引用的那个对象的某个公有字段值为另一个值
final MyClass obj = new MyClass();
还是前面的例子,如果定义了以上这个final对象变量,不能给obj重新赋值,但能更obj内的字段,比如value。
判等操作
对于原始数据类型的变量(比如int),可以直接使用“==”运算符判断两变量值是否相等:
当“==”施加于引用类型变量时,是比较这两个变量是否引用同一对象。
引用代表地址,所以“==”实际上相当于比较两个引用类型变量中保存的对象地址数值是否相同。
那么比较两个对象的“内容”是否一样(两个对象的“对应字段值”一致),有两种方法:
1. “重写(override)”基类的equals()方法
import java.util.Objects;
public class ObjectEquals {
public static void main(String[] args) {
MyTestClass obj1 = new MyTestClass(100);
MyTestClass obj2 = new MyTestClass(100);
System.out.println(obj1 == obj2);//false
boolean equalsResult = Objects.equals(obj1, obj2);
//以下这句的输出结果,取决于MyTestClass是否重写了equals方法
System.out.println(equalsResult); //false 或 true
}
}
class MyTestClass {
public int Value;
// 注意:只有参数类型为Object的,才是重写了Object的equals方法
// 参数类型为MyTestClass的,仅仅是Overload了equals方法。
// 这个方法是否被注释,会影响到Objects.equals的结果
@Override
public boolean equals(Object obj) {
return ((MyTestClass) obj).Value == this.Value;
}
public MyTestClass(int initValue) {
Value = initValue;
}
}
tips:左边这种重写equals()的方式是简化的,在实际开发中不应该这样写,至少应该加上对参数有效性的检测代码,另外还需要重写另一个获取对象hash值的方法。
"@Override"指明此方法是"重写基类的同名方法",Java中,这种以“@”打头的标记被称为“注解(Annotation)”。
当定义一个类时,如果不显式指明它的“父类” 是哪个,则默认是Object。
Object是Java的最顶层基类,其中定义了equals()方法。但是要使用这个判等的功能,需要对equals()方法重写,使其适用自己的类。
2. “重载(overload)”equals()方法
public class MyClass {
public static void main(String[] args) {
MyTestClass obj1 = new MyTestClass(100);
MyTestClass obj2 = new MyTestClass(100);
System.out.println(obj1 == obj2);//false
System.out.println(obj1.equals(obj2));//true
}
}
class MyTestClass {
public int Value;
public boolean equals(MyTestClass obj) {
return obj.Value == this.Value;
}
public MyTestClass(int initValue) {
Value = initValue;
}
}
tips:与前面“重写”方式的代码相比,equals()方法的参数类型是MyTestClass而不是Object,并且方法本身也没有附加“@Override”注解。
对象的构造
public class MyClass {
public static void main(String[] args) {
MyTestClass obj1 = new MyTestClass(100);
System.out.println(obj1.getValue());
}
}
class MyTestClass {
public int Value;
public MyTestClass(int initValue) {
Value = initValue;
}
public int getValue() {
return Value;
}
}
这个MyTestClass类内有一个特殊的方法:构造方法与类名相同,没有返回值。
这个方法被称为类的“构造方法(constructor)”,有时也习惯称为“构造函数”。
构造方法的特点:
- 当使用new关键字创建一个对象时,它的构造方法会被自动调用。
- 如果类没有定义构造函数,Java编译器在编译时会自动给它提供一个没有参数的“默认构造方法”。
- 构造方法不能是抽象的,静态的,最终的,同步的也就是说,他不能通过abstract,static,final,synchronized关键字修饰
- 这个有点难理解,解释一下,就是由于构造方法是不能被继承的,所以final和abstract是没有意义的,而且他是用于初始化一个对象的,所以static也是没有用的,多个线程也不会同时去创建内存地址相同的对象,所以锁也是没有意义的
为什么要叫做构造方法,是因为在对象创建的时候,需要公共构造方法去初始化值,去描述对象的那些状态,去对应对象中的字段。
总的来讲Java中只有两种构造方法:有参构造和无参构造
有参构造与无参构造
我前面的这个例子就是有参构造,在new一个新的对象时可以传入一个参数来给对象内的字段赋值。
public class MyClass {
public static void main(String[] args) {
MyTestClass obj1 = new MyTestClass(100,"123");
System.out.println("obj1.value: "+obj1.Value+" obj1.strvalue: "+obj1.strvalue);
}
}
class MyTestClass {
public int Value;
public String strvalue;
public MyTestClass(int Value ,String strvalue) {
this.Value = Value;
this.strvalue = strvalue;
}
}
像这样,可以在构造方法内传入多个参数。
如果类提供了一个自定义的构造方法,将导致编译器为其默认提供的无参构造方法失效。
上面这个例子中,就不能直接下面这样创建新对象了,需要另外重载一个无参构造方法
MyTestClass obj1 = new MyTestClass();
无参构造:
顾名思义,就是不往构造方法内传入参数,直接运行构造方法内的语句。可以在构造方法内对字段初始化。
构造方法重载
构造方法的重载和其他方法的重载一样,都是通过改变形参的 类型与数量来控制。
public class MyClass {
public static void main(String[] args) {
MyTestClass obj1 = new MyTestClass(100,"123");
System.out.println("obj1.value: "+obj1.Value+" obj1.strvalue: "+obj1.strvalue);
MyTestClass obj2 = new MyTestClass();
System.out.println("obj2.value: "+obj2.Value+" obj2.strvalue: "+obj2.strvalue);
}
}
class MyTestClass {
public int Value;
public String strvalue;
public MyTestClass(int Value ,String strvalue) {
this.Value = Value;
this.strvalue = strvalue;
}
public MyTestClass() {
this.Value = 0;
this.strvalue = "";
}
}
就像这个代码,重载了一个有参构造方法和一个无参构造方法。
同一个类的构造函数之间可以通过this关键字相互调用。
就像刚才的无参构造方法也可以写成这样
public MyTestClass() { this(0,""); }
也能这样写:
public class MyClass {
public static void main(String[] args) {
MyTestClass obj1 = new MyTestClass();
MyTestClass obj2 = new MyTestClass(1,2);
MyTestClass obj3 = new MyTestClass(1,2,3,4);
obj1.print();
obj2.print();
obj3.print();
}
}
class MyTestClass {
public int a;
public int b;
public int c;
public int d;
public MyTestClass() {
this(0,0);
}
public MyTestClass(int c,int d) {
this(0,0,c,d);
}
public MyTestClass(int a,int b,int c,int d) {
this.a=a;
this.b=b;
this.c=c;
this.d=d;
}
public void print() {
System.out.println("a="+a+",b="+b+",c="+c+",d="+d);
}
}
初始化
可以在类中使用“{”和“}”将语句包围起来,直接将其作为类的成员,称为“类的初始化块”。
public class MyClass {
public static void main(String[] args) {
MyTestClass obj1 = new MyTestClass();
obj1.print();
}
}
class MyTestClass {
public int a;
public int b;
public int c;
public int d;
{
a=1;
b=2;
c=3;
d=4;
}
public void print() {
System.out.println("a="+a+",b="+b+",c="+c+",d="+d);
}
}
如果同时拥有构造函数与初始化块,会按照构造函数内的参数来赋值。
public class MyClass {
public static void main(String[] args) {
MyTestClass obj1 = new MyTestClass(5,6,7,8);
obj1.print();
}
}
class MyTestClass {
public int a;
public int b;
public int c;
public int d;
{
a=1;
b=2;
c=3;
d=4;
}
public void print() {
System.out.println("a="+a+",b="+b+",c="+c+",d="+d);
}
public MyTestClass(int a,int b,int c,int d) {
this.a=a;
this.b=b;
this.c=c;
this.d=d;
}
}
实际开发中,应该尽量保证一个字段只初始化一次!不要同时使用构造函数和初始化块来初始化。
类的初始化块不接收任何的参数,而且只要一创建类的对象,它们就会被执行。因此,适合于封装那些“对象创建时必须执行的代码”。
创建一个子类的对象,会先调用父类的构造函数,再调用子类的构造函数。如果有多层的话,先调用最顶层的父类。不过继承部分之后再讲。(挖坑)
类的静态成员
静态字段
在类内可以用static关键字来修饰字段,这样的字段叫做静态字段。
可以用对象变量或类名作为前缀访问静态数据:
实际开发中,推荐使用类名而不是对象变量来访问静态字段(或调用静态方法)
静态常量
我们还可以将静态字段声明为 final 类型的,这样就变成了静态常量。静态常量的值是在编译期间确定的,并且只能被读取。通常用来表示不可修改的值,如 pi、e、最大连接数等。
public class MathHelper {
public static final double PI = 3.1415926;
public static double calculateCircleArea(double radius) {
return Math.pow(radius, 2) * PI;
}
}
double area = MathHelper.calculateCircleArea(5.0); // 计算半径为 5 的圆的面积
System.out.println(area); // 输出 78.53981633974483
静态初始化块
可以使用“静态初始化块”初始化类的静态字段:
静态初始化块只执行一次。
创建子类型的对象时,也会导致父类型的静态初始化块的执行。
静态方法
除了静态字段,Java 中还可以定义静态方法。静态方法是独立于任何对象的,可以访问静态字段和其他静态方法,但不能访问非静态成员变量和方法。静态方法常用于工具类和公用函数库中,它们和对象实例无关,可以直接通过类名来调用。
类的静态方法只能访问类的静态成员!
静态方法示例:
import java.util.Arrays;
public class ArrayHelper {
public static int[] reverse(int[] arr) {
int[] reversedArr = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
reversedArr[i] = arr[arr.length - 1 - i];
}
return reversedArr;
}
}
int[] arr = {1, 2, 3, 4};
int[] reversed = ArrayHelper.reverse(arr);
System.out.println(Arrays.toString(reversed)); // 输出 [4, 3, 2, 1]
在静态方法中访问类的实例成员(即那些没有附加static关键字的字段或方法):
工厂方法
另外一个常用的静态方法是工厂方法。工厂方法用于创建和返回新的对象实例,它通常被定义为 static 方法,可以不用通过类的实例化就可以访问它,并且会在内部调用私有构造函数创建需要的对象。工厂方法在设计模式中被广泛应用,它可以隐藏对象的具体创建过程,简化了代码的使用。
工厂方法示例:
import java.util.Arrays;
public class ArrayHelper {
public static int[] reverse(int[] arr) {
int[] reversedArr = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
reversedArr[i] = arr[arr.length - 1 - i];
}
return reversedArr;
}
}
int[] arr = {1, 2, 3, 4};
int[] reversed = ArrayHelper.reverse(arr);
System.out.println(Arrays.toString(reversed)); // 输出 [4, 3, 2, 1]
main 方法
在 Java 中,main 方法是程序的入口点,在执行时由 JVM 调用。main 方法必须声明为 public、static 和 void 类型,JVM 在启动程序时会首先加载包含 main 方法的类,并进行相应的初始化操作。main 方法也是一个典型的静态方法的例子,因为它定义在类中,而不需要通过类的实例来访问。
静态成员的使用
使用类的静态字段和构造函数,我们可以跟踪某个类所创建对象的个数。
public class ObjectCounter {
// 静态字段,用于跟踪对象数量
private static int objectCount = 0;
// 构造函数
public ObjectCounter() {
objectCount++; // 每次创建对象时增加计数
}
// 静态方法,用于查询当前对象数量
public static int getObjectCount() {
return objectCount;
}
public static void main(String[] args) {
// 创建一些对象
ObjectCounter obj1 = new ObjectCounter();
ObjectCounter obj2 = new ObjectCounter();
ObjectCounter obj3 = new ObjectCounter();
// 查询当前对象数量
System.out.println("当前创建对象的个数: " + ObjectCounter.getObjectCount());
}
}
但是这个类在多线程环境下可能会出现问题,因为多个线程同时访问和修改 objectCount
可能导致竞争条件,进而导致计数错误。为了在多线程环境中安全地使用这个类,可以使用同步机制来确保对 objectCount
的访问是线程安全的。
incrementCount()
方法使用synchronized
关键字来确保只有一个线程可以在同一时间访问它,从而避免了竞争条件。getObjectCount()
方法也被标记为synchronized
,以确保在获取对象计数时是线程安全的。
public class ObjectCounter {
// 静态字段,用于跟踪对象数量
private static int objectCount = 0;
// 构造函数
public ObjectCounter() {
incrementCount(); // 每次创建对象时增加计数
}
// 增加计数的同步方法
private synchronized void incrementCount() {
objectCount++;
}
// 静态方法,用于查询当前对象数量
public static synchronized int getObjectCount() {
return objectCount;
}
public static void main(String[] args) {
// 创建多个线程来测试
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
new ObjectCounter();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 查询当前对象数量
System.out.println("当前创建对象的个数: " + ObjectCounter.getObjectCount());
}
}
多线程的内容之后再细讲(继续挖坑)
枚举类型
枚举类型是一种特殊的数据类型,之所以特殊是因为它既是一种类(class)类型却又比类 类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。
举两个栗子:
public class Test {
public static void main(String[] args) {
Size s =Size.SMALL;
Size t = Size.MEDIUM;
System.out.println("s="+s);
System.out.println("s和t引用的是否同一个对象:" + (s == t));
System.out.println("s与t内容是否相同:" + s.equals(t));
System.out.println("s的类型: " + s.getClass());
System.out.println("是否是原始数据类型:" + s.getClass().isPrimitive());
//枚举Size内的所有值
for(Size value : Size.values()){
System.out.print(value+",");
}
}
}
enum Size{
SMALL,
MEDIUM,
LARGE
};
enum Day {
MONDAY("Monday"),
TUESDAY("Tuesday"),
WEDNESDAY("Wednesday"),
THURSDAY("Thursday"),
FRIDAY("Friday"),
SATURDAY("Saturday"),
SUNDAY("Sunday");
private final String name;
private Day(String name) {
this.name = name;
}
public String getName() {
return name;
}
public boolean isWeekend() {
return this == SATURDAY || this == SUNDAY;
}
}
public class Test{
public static void main(String[] args) {
for (Day day : Day.values()) {
System.out.println(day);
// System.out.println(day.isWeekend());
}
}
}
enum这个关键字,可以理解为跟class差不多,这也个单独的类。可以看到,上面的例子里面有属性,有构造方法,有getter,也可以有setter,但是一般都是构造传参数。还有其他自定义方法。
enum Size{ SMALL, MEDIUM, LARGE };
在大括号内的这部分叫做,这个枚举的实例。也可以理解为,class new 出来的实例对象。
这下就好理解了。只是,class,new对象,可以自己随便new,想几个就几个,而这个enum关键字,他就不行,他的实例对象,只能在这个enum里面体现。也就是说,他对应的实例是有限的。这也就是枚举的好处了,限制了某些东西的范围,比如:一周只能有七天,用枚举类型的话,你在enum的大括号里面把所有的选项,全列出来,那么这个一周的属性,对应的值,只能在里面挑。不能有其他的。
- 枚举类型是引用类型!
- 枚举不属于原始数据类型,它的每个具体值都引用一个特定的对象,相同的值则引用同一个对象。
- 可以使用“==”和“equals()”方法直接比对枚举变量的值,换句话说,对于枚举类型的变量,“==”和“equals()”方法执行的结果是等价的。
包装类
因为Java是一个完全的面向对象的语言,几乎所有的方法都可以直接通过对象.方法()调用,然而8种基本数据类型的存在就很鸡肋,不能直接用int. char. double.来调用想用的功能,也没有继承封装等面向对象的思想,因此引入了包装类:
包装类 = 基本数据类型的数据 + 扩充的一些方法和字段
包装类是引用类型,可以使用new关键字创建相应的对象实例
除了int和char,其他的都是直接首字母大写,前6个数值型的包装类都有一个共同的父类Number,Number又继承于Object
父类Number方法
方法名 | 作用 |
---|---|
byte byteValue() | 以 byte 形式返回指定的数值 |
abstract double doubleValue() | 以 double 形式返回指定的数值 |
abstract float floatValue() | 以 float 形式返回指定的数值 |
abstract int intValue() | 以 int形式返回指定的数值 |
abstract long longValue() | 以 long形式返回指定的数值 |
short shortValue() | 以 short形式返回指定的数值 |
手动装箱/手动拆箱
public class IntegerTest02 {
public static void main(String[] args) {
// 123这个基本数据类型,进行构造方法的包装达到了:基本数据类型向引用数据类型的转换。
// 基本数据类型 -(转换为)->引用数据类型(装箱)
Integer i = new Integer(123);
// 将引用数据类型--(转换为)-> 基本数据类型
float f = i.floatValue();
System.out.println(f); //123.0
// 将引用数据类型--(转换为)-> 基本数据类型(拆箱)
int retValue = i.intValue();
System.out.println(retValue); //123
}
}
int value = 100;
Integer obj = value; //装箱
int result = obj * 2; //拆箱
栗子:
public class MyClass {
public static void main(String[] args) {
// 123这个基本数据类型,进行构造方法的包装达到了:基本数据类型向引用数据类型的转换。
// 基本数据类型 -(转换为)->引用数据类型(装箱)
Integer i = new Integer(123);
Integer j = new Integer("123");
System.out.println("i="+i);
System.out.println("j="+j);
// 将引用数据类型--(转换为)-> 基本数据类型
float f = i.floatValue();
System.out.println("f="+f); //123.0
// 将引用数据类型--(转换为)-> 基本数据类型(拆箱)
int k = i.intValue();
System.out.println("k="+k); //123
Boolean A = new Boolean(true);//加不加双引号都行,加了就自动String转boolean
Boolean B = new Boolean("TrUe");//Boolean包装类定义构造的时候忽略大小写
Boolean C = new Boolean("qwer123");//其他的所有情况都是false
System.out.println("A="+A.toString());
System.out.println("B="+B.toString());
System.out.println("C="+C.toString());
}
}
这8种包装类都有这类似的一句“提供String转换方法”,即在初始化构造的时候可以在()种输入String类型,并且每个包装类都有独自的转换方式
tips:编译器会提示从Java 9开始,new Integer(int)和 new Boolean(String)
这一类直接使用构造方法的方式已被弃用。这是因为这些构造方法没有提供比静态工厂方法(如 Integer.valueOf(int)
和 Boolean.valueOf(String)
)更好的性能或更清晰的意图,后者更建议使用。不过上面的代码仍能编译运行。但建议写成下面这样 ↓
public class MyClass {
public static void main(String[] args) {
// 使用静态工厂方法进行装箱
Integer i = Integer.valueOf(123);
Integer j = Integer.valueOf("123");
System.out.println("i=" + i);
System.out.println("j=" + j);
// 将引用数据类型--(转换为)-> 基本数据类型
float f = i.floatValue();
System.out.println("f=" + f); // 123.0
// 将引用数据类型--(转换为)-> 基本数据类型(拆箱)
int k = i.intValue();
System.out.println("k=" + k); // 123
// 使用静态工厂方法
Boolean A = Boolean.valueOf(true);
Boolean B = Boolean.valueOf("TrUe"); // 提示:这种方式是忽略大小写
Boolean C = Boolean.valueOf("qwer123"); // 其他的所有情况都是false
System.out.println("A=" + A.toString());
System.out.println("B=" + B.toString());
System.out.println("C=" + C.toString());
}
}
javap反编译
写一个简单的拆装箱
public class BoxAndUnbox {
public static void main(String[] args) {
int value = 100;
Integer obj = value; //装箱
int result = obj * 2; //拆箱
System.out.println(result);
}
}
使用javap反编译class文件:javap -c 字节码文件
javap是JDK提供的一个反汇编工具。
装箱:
Integer.valueOf()
拆箱:
Integer.intValue()
缓存
public class MyClass {
public static void main(String[] args) {
Integer i1 = 100;
Integer j1 = 100;
System.out.println(i1 == j1);
Integer i2 = 129;
Integer j2 = 129;
System.out.println(i2 == j2);
}
}
编译运行这个代码,会发现前一个输出的为true,后一个输出为false,非常神奇。
这是因为java 对于小整数有一个特殊的优化,称为缓存。
1. Integer 缓存
Java 对 Integer
实例在 -128
到 127
的范围内进行缓存。当你使用 Integer
的自动装箱(如 Integer i = 100;
)时,如果该值落在缓存范围内,Java 会返回一个已经存在的 Integer
实例,而不是创建一个新的实例。因此,在这个范围内的整数,即使它们在不同的地方被赋值,也会指向同一个 Integer
对象。
Integer i1 = 100; // 100 在缓存范围内
Integer j1 = 100; // 使用相同的缓存实例
System.out.println(i1 == j1); // 输出 true,因为 i1 和 j1 引用同一个对象
2. 超出缓存范围
对于超出 127
的整数值,Java 不会使用缓存,而是每次都会创建一个新的 Integer
实例。
Integer i2 = 129; // 129 超出缓存范围,创建新的实例
Integer j2 = 129; // 再次创建新的实例
System.out.println(i2 == j2); // 输出 false,因为 i2 和 j2 引用不同的对象
- 对于
-128
到127
范围内的整数,Integer
实例会被缓存,因此相等比较(==
)会返回true
。二者引用的是同一个实例。 - 对于超出这个范围的整数,每次都会创建新的实例,导致相等比较返回
false
。
如果想要比较两个 Integer
的值是否相等,应该使用 equals
方法
System.out.println(i2.equals(j2)); // 输出 true,因为它们的值相同