-
2021-02-12 23:17:19
怎么写
java构造方法必须满足以下语法规则:
(1) 方法名必须与类名相同。
(2)不要声明返回类型。
(3)不能被static、final、synchronized、abstract和native修饰。构造方法不能被子类继承,所以用final和abstract修饰没有意义。
构造方法用于初始化一个新建的对象,所以用static修饰没有意义。多个线程不会同时创建内存地址相同的同一个对象,因此用synchronized修饰没有必要。
此外,Java语言不支持native类型的构造方法。
例:
public class Sample {
private int x;
public Sample() { // 不带参数的构造方法
this(1);
}
public Sample(int x) { //带参数的构造方法
this.x=x;
}
public int Sample(int x) { //不是构造方法
return x++;
}
}
使用方法
java构造方法作用就是对类进行初始化。 如果你没有定议任何构造方法的形式,程式会为你取一个不带任何参数的构造函数,那么你产生类的对像时只能用不带参数的方法,如:class a {}//没有任何构造函数。
构造方法就是与类同名的那个方法,它的作用是可以用来初始化,例子如下
class Person //人类{
public Person(String n,int a) //构造方法
{
name = n; age = a;
}
private string name;
private int age;
}
static void main(String[] args){
Person p = new Person("张三",14);//这就是作用
}
new一个对象的时候要用到构造函数,
例如
Hello hello = new Hello();
这时调用的是Hello的无参数构造方法;
Hello hello = new Hello("hi");
这个是调用Hello有参数构造方法,
在JAVA中如果不写构造方法的话,会默认加上一个无参数的构造方法,但是如果已经有了一个有参数的构造方法,那么无参数的构造方法就不会默认被加上.
如果Hello类中已经有了一个有参数的构造方法,这时再使用 Hello hello = new Hello();来创建对象的时候就会出错,这就是为什么书上要强调写了有参数的构造方法就最好加一个无参数的构造方法.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。
更多相关内容 -
简单谈谈java中匿名内部类构造函数
2020-09-03 04:50:50主要简单给我们介绍了java中匿名内部类构造函数,并附上了简单的示例,有需要的小伙伴可以参考下。 -
java怎么调用构造方法
2021-03-04 04:38:04Java调用构造方法,那就是子类调用基类(父类)的构造方法,如何调用,请看详细内容。子类中的构造方法的规则子类的构造过程中必须调用其基类的构造方法。子类可以在自己的构造方法中使用super(argument_list)调用基类...Java调用构造方法,那就是子类调用基类(父类)的构造方法,如何调用,请看详细内容。
子类中的构造方法的规则
子类的构造过程中必须调用其基类的构造方法。
子类可以在自己的构造方法中使用super(argument_list)调用基类的构造方法。
如果使用this(argument_list)调用本类的另外构造方法。
如果调用super,必须写在子类构造方法的第一行。
如果子类的构造方法中没有显示的调用基类的构造方法,则系统默认调用基类的无参数构造方法。
如果子类构造方法中既没有显示调用基类构造方法,而基类又没有无参数的构造方法,则编译出错。
示例如下:class SuperClass{
private int n;
//SuperClass(){
// System.out.println("SuperClass()");
//}
SuperClass(int n){
System.out.println("SuperClass(int n)");
this.n = n;
}
}
class SubClass extends SuperClass{
private int n;
SubClass(){
super(300);//调用父类的有参构造方法
System.out.println("SuperClass");
}
SubClass(int n){
System.out.println("SubClass(int n):"+n);
this.n = n;
}
}
public class TestSuperSub{
public static void main (String args[]){
//SubClass sc = new SubClass();
SubClass sc2 = new SubClass(200);
}
}
依次验证上面的语法。
-
Java里子类调用父类构造方法问题
2021-02-13 01:44:43在Java中,子类的构造过程中,必须调用其父类的构造函数,是因为有继承关系存在时,子类要把父类的内容继承下来,通过什么手段做到的?...构造方法是new一个对象的时候,必须要调的方法,这是规定,要new父类对象出...在Java中,子类的构造过程中,必须调用其父类的构造函数,是因为有继承关系存在时,子类要把父类的内容继承下来,通过什么手段做到的?
答案如下:
当你new一个子类对象的时候,必须首先要new一个父类的对像出来,这个父类对象位于子类对象的内部,所以说,子类对象比父类对象大,子类对象里面包含了一个父类的对象,这是内存中真实的情况.构造方法是new一个对象的时候,必须要调的方法,这是规定,要new父类对象出来,那么肯定要调用其构造方法,所以:
第一个规则:子类的构造过程中,必须调用其父类的构造方法。一个类,如果我们不写构造方法,那么编译器会帮我们加上一个默认的构造方法,所谓默认的构造方法,就是没有参数的构造方法,但是如果你自己写了构造方法,那么编译器就不会给你添加了,所以有时候当你new一个子类对象的时候,肯定调用了子类的构造方法,但是在子类构造方法中我们并没有显示的调用基类的构造方法,就是没写,如:super(); 并没有这样写,但是这样就会调用父类没有参数的构造方法,如果父类中没有没有参数的构造方法就会出错。
第二个规则:如果子类的构造方法中没有显示的调用基类构造方法,则系统默认调用基类无参数的构造方法注意:如果子类的构造方法中既没有显示的调用基类构造方法,而基类中又没有默认无参的构造方法,则编译出错,所以,通常我们需要显示的:super(参数列表),来调用父类有参数的构造函数。
1 //当你没有使用父类默认的构造方法时,此时在子类的构造方法中就需要显示的调用父类定义的构造方法。
2 classAnimal{3 privateString name;4
5 //如果你定义一个新的构造方法
6 publicAnimal(String name) {7 this.name =name;8 }9 }10
11 public Dog extendsAnimal{12
13 //这时你就要显示的调用父类的构造方法,因为子类默认调用的是父类的14 //无参构造方法Animal()
15 publicDog(){16 super("小狗"); //显示调用父类的有参构造方法
17
18 .... //子类的构造方法处理
19 }20 }21
22 //当然,如果你在父类里面把无参的构造方法,显示的写出来了,比如:
23 classAnimal{24 privateString name;25
26 //无参的构造方法
27 publicAnimal() {28 ..... //处理
29 }30
31 /*
32 如果你定义一个新的构造方法,那么在子类的构造方法中,就可以不用显示的调用父类的构造方法,因为子类有个无参的构造方法,33 子类在构造方法中会自动调用父类已经定义的无参构造方法。34 */
35 publicAnimal(String name) {36 this.name =name;37 }38 }
总结:不过一般的,在父类中使用了构造方法的重载,在子类中就可以根据需要,调用相应的父类构造方法。
-
详解 Java 内部类
2018-09-04 00:17:56内部类在 Java 里面算是非常常见的一个功能了,在日常开发中我们肯定多多少少都用过,这里总结一下关于 Java 中内部类的相关知识点和一些使用内部类时需要注意的点。 从种类上说,内部类可以分为四类:普通内部类...*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
前言
内部类在 Java 里面算是非常常见的一个功能了,在日常开发中我们肯定多多少少都用过,这里总结一下关于 Java 中内部类的相关知识点和一些使用内部类时需要注意的点。
从种类上说,内部类可以分为四类:普通内部类、静态内部类、匿名内部类、局部内部类。我们来一个个看:注
本文所有实践代码在 JDK1.8 版本进行验证。
普通内部类
这个是最常见的内部类之一了,其定义也很简单,在一个类里面作为类的一个字段直接定义就可以了,例:
public class InnerClassTest { public class InnerClassA { } }
在这里
InnerClassA
类为InnerClassTest
类的普通内部类,在这种定义方式下,普通内部类对象依赖外部类对象而存在,即在创建一个普通内部类对象时首先需要创建其外部类对象,我们在创建上面代码中的InnerClassA
对象时先要创建InnerClassTest
对象,例:public class InnerClassTest { public int outField1 = 1; protected int outField2 = 2; int outField3 = 3; private int outField4 = 4; public InnerClassTest() { // 在外部类对象内部,直接通过 new InnerClass(); 创建内部类对象 InnerClassA innerObj = new InnerClassA(); System.out.println("创建 " + this.getClass().getSimpleName() + " 对象"); System.out.println("其内部类的 field1 字段的值为: " + innerObj.field1); System.out.println("其内部类的 field2 字段的值为: " + innerObj.field2); System.out.println("其内部类的 field3 字段的值为: " + innerObj.field3); System.out.println("其内部类的 field4 字段的值为: " + innerObj.field4); } public class InnerClassA { public int field1 = 5; protected int field2 = 6; int field3 = 7; private int field4 = 8; // static int field5 = 5; // 编译错误!普通内部类中不能定义 static 属性 public InnerClassA() { System.out.println("创建 " + this.getClass().getSimpleName() + " 对象"); System.out.println("其外部类的 outField1 字段的值为: " + outField1); System.out.println("其外部类的 outField2 字段的值为: " + outField2); System.out.println("其外部类的 outField3 字段的值为: " + outField3); System.out.println("其外部类的 outField4 字段的值为: " + outField4); } } public static void main(String[] args) { InnerClassTest outerObj = new InnerClassTest(); // 不在外部类内部,使用:外部类对象. new 内部类构造器(); 的方式创建内部类对象 // InnerClassA innerObj = outerObj.new InnerClassA(); } }
这里的内部类就像外部类声明的一个属性字段一样,因此其的对象时依附于外部类对象而存在的,我们来看一下结果:
我们注意到,内部类对象可以访问外部类对象中所有访问权限的字段,同时,外部类对象也可以通过内部类的对象引用来访问内部类中定义的所有访问权限的字段,后面我们将从源码里面分析具体的原因。
我们下面来看一下静态内部类:静态内部类
我们知道,一个类的静态成员独立于这个类的任何一个对象存在,只要在具有访问权限的地方,我们就可以通过
类名.静态成员名
的形式来访问这个静态成员,同样的,静态内部类也是作为一个外部类的静态成员而存在,创建一个类的静态内部类对象不需要依赖其外部类对象。例:public class InnerClassTest { public int field1 = 1; public InnerClassTest() { System.out.println("创建 " + this.getClass().getSimpleName() + " 对象"); // 创建静态内部类对象 StaticClass innerObj = new StaticClass(); System.out.println("其内部类的 field1 字段的值为: " + innerObj.field1); System.out.println("其内部类的 field2 字段的值为: " + innerObj.field2); System.out.println("其内部类的 field3 字段的值为: " + innerObj.field3); System.out.println("其内部类的 field4 字段的值为: " + innerObj.field4); } static class StaticClass { public int field1 = 1; protected int field2 = 2; int field3 = 3; private int field4 = 4; // 静态内部类中可以定义 static 属性 static int field5 = 5; public StaticClass() { System.out.println("创建 " + StaticClass.class.getSimpleName() + " 对象"); // System.out.println("其外部类的 field1 字段的值为: " + field1); // 编译错误!! } } public static void main(String[] args) { // 无需依赖外部类对象,直接创建内部类对象 // InnerClassTest.StaticClass staticClassObj = new InnerClassTest.StaticClass(); InnerClassTest outerObj = new InnerClassTest(); } }
结果:
可以看到,静态内部类就像外部类的一个静态成员一样,创建其对象无需依赖外部类对象(访问一个类的静态成员也无需依赖这个类的对象,因为它是独立于所有类的对象的)。但是于此同时,静态内部类中也无法访问外部类的非静态成员,因为外部类的非静态成员是属于每一个外部类对象的,而本身静态内部类就是独立外部类对象存在的,所以静态内部类不能访问外部类的非静态成员,而外部类依然可以访问静态内部类对象的所有访问权限的成员,这一点和普通内部类无异。
匿名内部类
匿名内部类有多种形式,其中最常见的一种形式莫过于在方法参数中新建一个接口对象 / 类对象,并且实现这个接口声明 / 类中原有的方法了:
public class InnerClassTest { public int field1 = 1; protected int field2 = 2; int field3 = 3; private int field4 = 4; public InnerClassTest() { System.out.println("创建 " + this.getClass().getSimpleName() + " 对象"); } // 自定义接口 interface OnClickListener { void onClick(Object obj); } private void anonymousClassTest() { // 在这个过程中会新建一个匿名内部类对象, // 这个匿名内部类实现了 OnClickListener 接口并重写 onClick 方法 OnClickListener clickListener = new OnClickListener() { // 可以在内部类中定义属性,但是只能在当前内部类中使用, // 无法在外部类中使用,因为外部类无法获取当前匿名内部类的类名, // 也就无法创建匿名内部类的对象 int field = 1; @Override public void onClick(Object obj) { System.out.println("对象 " + obj + " 被点击"); System.out.println("其外部类的 field1 字段的值为: " + field1); System.out.println("其外部类的 field2 字段的值为: " + field2); System.out.println("其外部类的 field3 字段的值为: " + field3); System.out.println("其外部类的 field4 字段的值为: " + field4); } }; // new Object() 过程会新建一个匿名内部类,继承于 Object 类, // 并重写了 toString() 方法 clickListener.onClick(new Object() { @Override public String toString() { return "obj1"; } }); } public static void main(String[] args) { InnerClassTest outObj = new InnerClassTest(); outObj.anonymousClassTest(); } }
来看看结果:
上面的代码中展示了常见的两种使用匿名内部类的情况:
1、直接 new 一个接口,并实现这个接口声明的方法,在这个过程其实会创建一个匿名内部类实现这个接口,并重写接口声明的方法,然后再创建一个这个匿名内部类的对象并赋值给前面的OnClickListener
类型的引用;
2、new 一个已经存在的类 / 抽象类,并且选择性的实现这个类中的一个或者多个非 final 的方法,这个过程会创建一个匿名内部类对象继承对应的类 / 抽象类,并且重写对应的方法。同样的,在匿名内部类中可以使用外部类的属性,但是外部类却不能使用匿名内部类中定义的属性,因为是匿名内部类,因此在外部类中无法获取这个类的类名,也就无法得到属性信息。
局部内部类
局部内部类使用的比较少,其声明在一个方法体 / 一段代码块的内部,而且不在定义类的定义域之内便无法使用,其提供的功能使用匿名内部类都可以实现,而本身匿名内部类可以写得比它更简洁,因此局部内部类用的比较少。来看一个局部内部类的小例子:
public class InnerClassTest { public int field1 = 1; protected int field2 = 2; int field3 = 3; private int field4 = 4; public InnerClassTest() { System.out.println("创建 " + this.getClass().getSimpleName() + " 对象"); } private void localInnerClassTest() { // 局部内部类 A,只能在当前方法中使用 class A { // static int field = 1; // 编译错误!局部内部类中不能定义 static 字段 public A() { System.out.println("创建 " + A.class.getSimpleName() + " 对象"); System.out.println("其外部类的 field1 字段的值为: " + field1); System.out.println("其外部类的 field2 字段的值为: " + field2); System.out.println("其外部类的 field3 字段的值为: " + field3); System.out.println("其外部类的 field4 字段的值为: " + field4); } } A a = new A(); if (true) { // 局部内部类 B,只能在当前代码块中使用 class B { public B() { System.out.println("创建 " + B.class.getSimpleName() + " 对象"); System.out.println("其外部类的 field1 字段的值为: " + field1); System.out.println("其外部类的 field2 字段的值为: " + field2); System.out.println("其外部类的 field3 字段的值为: " + field3); System.out.println("其外部类的 field4 字段的值为: " + field4); } } B b = new B(); } // B b1 = new B(); // 编译错误!不在类 B 的定义域内,找不到类 B, } public static void main(String[] args) { InnerClassTest outObj = new InnerClassTest(); outObj.localInnerClassTest(); } }
同样的,在局部内部类里面可以访问外部类对象的所有访问权限的字段,而外部类却不能访问局部内部类中定义的字段,因为局部内部类的定义只在其特定的方法体 / 代码块中有效,一旦出了这个定义域,那么其定义就失效了,就像代码注释中描述的那样,即外部类不能获取局部内部类的对象,因而无法访问局部内部类的字段。最后看看运行结果:
内部类的嵌套
内部类的嵌套,即为内部类中再定义内部类,这个问题从内部类的分类角度去考虑比较合适:
普通内部类:在这里我们可以把它看成一个外部类的普通成员方法,在其内部可以定义普通内部类(嵌套的普通内部类),但是无法定义static
修饰的内部类,就像你无法在成员方法中定义static
类型的变量一样,当然也可以定义匿名内部类和局部内部类;静态内部类:因为这个类独立于外部类对象而存在,我们完全可以将其拿出来,去掉修饰它的
static
关键字,他就是一个完整的类,因此在静态内部类内部可以定义普通内部类,也可以定义静态内部类,同时也可以定义static
成员;匿名内部类:和普通内部类一样,定义的普通内部类只能在这个匿名内部类中使用,定义的局部内部类只能在对应定义域内使用;
局部内部类:和匿名内部类一样,但是嵌套定义的内部类只能在对应定义域内使用。
深入理解内部类
不知道小伙伴们对上面的代码有没有产生疑惑:非静态内部类可以访问外部类所有访问权限修饰的字段(即包括了
private
权限的),同时,外部类也可以访问内部类的所有访问权限修饰的字段。而我们知道,private
权限的字段只能被当前类本身访问。然而在上面我们确实在代码中直接访问了对应外部类 / 内部类的private
权限的字段,要解除这个疑惑,只能从编译出来的类下手了,为了简便,这里采用下面的代码进行测试:public class InnerClassTest { int field1 = 1; private int field2 = 2; public InnerClassTest() { InnerClassA inner = new InnerClassA(); int v = inner.x2; } public class InnerClassA { int x1 = field1; private int x2 = field2; } }
我在外部类中定义了一个默认访问权限(同一个包内的类可以访问)的字段
field1
, 和一个private
权限的字段field2
,并且定义了一个内部类InnerClassA
,并且在这个内部类中也同样定义了两个和外部类中定义的相同修饰权限的字段,并且访问了外部类对应的字段。最后在外部类的构造方法中我定义了一个方法内变量赋值为内部类中private
权限的字段。我们用javac
命令(javac InnerClassTest.java
)编译这个.java
文件,会得到两个.classs
文件:
InnerClassTest.class
和InnerClassTest$InnerClassA.class
,我们再用javap -c
命令(javap -c InnerClassTest
和javap -c InnerClassTest$InnerClassA
)分别反编译这两个.class
文件,InnerClassTest.class
的字节码如下:
我们注意到字节码中多了一个默认修饰权限并且名为access$100
的静态方法,其接受一个InnerClassTest
类型的参数,即其接受一个外部类对象作为参数,方法内部用三条指令取到参数对象的field2
字段的值并返回。由此,我们现在大概能猜到内部类对象是怎么取到外部类的private
权限的字段了:就是通过这个外部类提供的静态方法。
类似的,我们注意到 24 行字节码指令invokestatic
,这里代表执行了一个静态方法,而后面的注释也写的很清楚,调用的是InnerClassTest$InnerClassA.access$000
方法,即调用了内部类中名为access$000
的静态方法,根据我们上面的外部类字节码规律,我们也能猜到这个方法就是内部类编译过程中编译器自动生成的,那么我们赶紧来看一下InnerClassTest$InnerClassA
类的字节码吧:
果然,我们在这里发现了名为access$000
的静态方法,并且这个静态方法接受一个InnerClassTest$InnerClassA
类型的参数,方法的作用也很简单:返回参数代表的内部类对象的x2
字段值。
我们还注意到编译器给内部类提供了一个接受InnerClassTest
类型对象(即外部类对象)的构造方法,内部类本身还定义了一个名为this$0
的InnerClassTest
类型的引用,这个引用在构造方法中指向了参数所对应的外部类对象。
最后,我们在 25 行字节码指令发现:内部类的构造方法通过invokestatic
指令执行外部类的access$100
静态方法(在InnerClassTest
的字节码中已经介绍了)得到外部类对象的field2
字段的值,并且在后面赋值给x2
字段。这样的话内部类就成功的通过外部类提供的静态方法得到了对应外部类对象的field2
。上面我们只是对普通内部类进行了分析,但其实匿名内部类和局部内部类的原理和普通内部类是类似的,只是在访问上有些不同:外部类无法访问匿名内部类和局部内部类对象的字段,因为外部类根本就不知道匿名内部类 / 局部内部类的类型信息(匿名内部类的类名被隐匿,局部内部类只能在定义域内使用)。但是匿名内部类和局部内部类却可以访问外部类的私有成员,原理也是通过外部类提供的静态方法来得到对应外部类对象的私有成员的值。而对于静态内部类来说,因为其实独立于外部类对象而存在,因此编译器不会为静态内部类对象提供外部类对象的引用,因为静态内部类对象的创建根本不需要外部类对象支持。但是外部类对象还是可以访问静态内部类对象的私有成员,因为外部类可以知道静态内部类的类型信息,即可以得到静态内部类的对象,那么就可以通过静态内部类提供的静态方法来获得对应的私有成员值。来看一个简单的代码证明:
public class InnerClassTest { int field1 = 1; private int field2 = 2; public InnerClassTest() { InnerClassA inner = new InnerClassA(); int v = inner.x2; } // 这里改成了静态内部类,因而不能访问外部类的非静态成员 public static class InnerClassA { private int x2 = 0; } }
同样的编译步骤,得到了两个
.class
文件,这里看一下内部类的.class
文件反编译的字节码InnerClassTest$InnerClassA
:
仔细看一下,确实没有找到指向外部类对象的引用,编译器只为这个静态内部类提供了一个无参构造方法。
而且因为外部类对象需要访问当前类的私有成员,编译器给这个静态内部类生成了一个名为access$000
的静态方法,作用已不用我多说了。如果我们不看类名,这个类完全可以作为一个普通的外部类来看,这正是静态内部类和其余的内部类的区别所在:静态内部类对象不依赖其外部类对象存在,而其余的内部类对象必须依赖其外部类对象而存在。OK,到这里问题都得到了解释:在非静态内部类访问外部类私有成员 / 外部类访问内部类私有成员 的时候,对应的外部类 / 外部类会生成一个静态方法,用来返回对应私有成员的值,而对应外部类对象 / 内部类对象通过调用其内部类 / 外部类提供的静态方法来获取对应的私有成员的值。
内部类和多重继承
我们已经知道,Java 中的类不允许多重继承,也就是说 Java 中的类只能有一个直接父类,而 Java 本身提供了内部类的机制,这是否可以在一定程度上弥补 Java 不允许多重继承的缺陷呢?我们这样来思考这个问题:假设我们有三个基类分别为
A
、B
、C
,我们希望有一个类D
达成这样的功能:通过这个D
类的对象,可以同时产生A
、B
、C
类的对象,通过刚刚的内部类的介绍,我们也应该想到了怎么完成这个需求了,创建一个类D.java
:class A {} class B {} class C {} public class D extends A { // 内部类,继承 B 类 class InnerClassB extends B { } // 内部类,继承 C 类 class InnerClassC extends C { } // 生成一个 B 类对象 public B makeB() { return new InnerClassB(); } // 生成一个 C 类对象 public C makeC() { return new InnerClassC(); } public static void testA(A a) { // ... } public static void testB(B b) { // ... } public static void testC(C c) { // ... } public static void main(String[] args) { D d = new D(); testA(d); testB(d.makeB()); testC(d.makeC()); } }
程序正确运行。而且因为普通内部类可以访问外部类的所有成员并且外部类也可以访问普通内部类的所有成员,因此这种方式在某种程度上可以说是 Java 多重继承的一种实现机制。但是这种方法也是有一定代价的,首先这种结构在一定程度上破坏了类结构,一般来说,建议一个
.java
文件只包含一个类,除非两个类之间有非常明确的依赖关系(比如说某种汽车和其专用型号的轮子),或者说一个类本来就是为了辅助另一个类而存在的(比如说上篇文章介绍的HashMap
类和其内部用于遍历其元素的HashIterator
类),那么这个时候使用内部类会有较好代码结构和实现效果。而在其他情况,将类分开写会有较好的代码可读性和代码维护性。内部类和内存泄露
在这一小节开始前介绍一下什么是内存泄露:即指在内存中存在一些其内存空间可以被回收的对象因为某些原因又没有被回收,因此产生了内存泄露,如果应用程序频繁发生内存泄露可能会产生很严重的后果(内存中可用的空间不足导致程序崩溃,甚至导致整个系统卡死)。
听起来怪吓人的,这个问题在一些需要开发者手动申请和释放内存的编程语言(C/C++)中会比较容易产生,因为开发者申请的内存需要手动释放,如果忘记了就会导致内存泄露,举个简单的例子(C++):#include <iostream> int main() { // 申请一段内存,空间为 100 个 int 元素所占的字节数 int *p = new int[100]; // C++ 11 p = nullptr; return 0; }
在这段代码里我有意而为之:在为指针 p 申请完内存之后将其直接赋值为
nullptr
,这是C++ 11
中一个表示空指针的关键字,我们平时常用的 NULL 只是一个值为 0 的常量值,在进行方法重载传参的时候可能会引起混淆。之后我直接返回了,虽然在程序结束之后操作系统会回收我们程序中申请的内存,但是不可否认的是上面的代码确实产生了内存泄露(申请的 100 个 int 元素所占的内存无法被回收)。这只是一个最简单不过的例子。我们在写这类程序的时候当动态申请的内存不再使用时,应该要主动释放申请的内存:#include <iostream> int main() { // 申请一段内存,空间为 100 个 int 元素所占的字节数 int *p = new int[100]; // 释放 p 指针所指向的内存空间 delete[] p; // C++ 11 p = nullptr; return 0; }
而在 Java 中,因为 JVM 有垃圾回收功能,对于我们自己创建的对象无需手动回收这些对象的内存空间,这种机制确实在一定程度上减轻了开发者的负担,但是也增加了开发者对 JVM 垃圾回收机制的依赖性,从某个方面来说,也是弱化了开发者防止内存泄露的意识。当然,JVM 的垃圾回收机制的利是远远大于弊的,只是我们在开发过程中不应该丧失了这种对象和内存的意识。
回到正题,内部类和内存泄露又有什么关系呢?在继续阅读之前,请确保你对 JVM 的在进行垃圾回收时如何找出内存中不再需要的对象有一定的了解,如果你对这个过程不太了解,你可以参考一下 这篇文章 中对这个过程的简单介绍。我们在上面已经知道了,创建非静态内部类的对象时,新建的非静态内部类对象会持有对外部类对象的引用,这个我们在上面的源码反编译中已经介绍过了,正是因为非静态内部类对象会持有外部类对象的引用,因此如果说这个非静态内部类对象因为某些原因无法被回收,就会导致这个外部类对象也无法被回收,这个听起来是有道理的,因为我们在上文也已经介绍了:非静态内部类对象依赖于外部类对象而存在,所以内部类对象没被回收,其外部类对象自然也不能被回收。但是可能存在这种情况:非静态内部类对象在某个时刻已经不在被使用,或者说这个内部类对象可以在不影响程序正确运行的情况下被回收,而因为我们对这个内部类的使用不当而使得其无法被 JVM 回收,同时会导致其外部类对象无法被回收,即为发生内存泄露。那么这个 “使用不当” 具体指的是哪个方面呢?看一个简单的例子,新建一个
MemoryLeakTest
的类:public class MemoryLeakTest { // 抽象类,模拟一些组件的基类 abstract static class Component { final void create() { onCreate(); } final void destroy() { onDestroy(); } // 子类实现,模拟组件创建的过程 abstract void onCreate(); // 子类实现,模拟组件摧毁的过程 abstract void onDestroy(); } // 具体某个组件 static class MyComponent extends Component { // 组件中窗口的单击事件监听器 static OnClickListener clickListener; // 模拟组件中的窗口 MyWindow myWindow; @Override void onCreate() { // 执行组件内一些资源初始化的代码 clickListener = new OnClickListener() { @Override public void onClick(Object obj) { System.out.println("对象 " + obj + " 被单击"); } }; // 新建我的窗口对象,并设置其单击事件监听器 myWindow = new MyWindow(); myWindow.setClickListener(clickListener); } @Override void onDestroy() { // 执行组件内一些资源回收的代码 myWindow.removeClickListener(); } } // 我的窗口类,模拟一个可视化控件 static class MyWindow { OnClickListener clickListener; // 设置当前控件的单击事件监听器 void setClickListener(OnClickListener clickListener) { this.clickListener = clickListener; } // 移除当前控件的单击事件监听器 void removeClickListener() { this.clickListener = null; } } // 对象的单击事件的监听接口 public interface OnClickListener { void onClick(Object obj); } public static void main(String[] args) { MyComponent myComponent = new MyComponent(); myComponent.create(); myComponent.destroy(); // myComponent 引用置为 null,排除它的干扰 myComponent = null; // 调用 JVM 的垃圾回收动作,回收无用对象 System.gc(); System.out.println(""); } }
我们在代码中添加一些断点,然后采用 debug 模式查看:
程序执行到 72 行代码,此时 72 行代码还未执行,因此myComponent
引用和其对象还未创建,继续执行:
这里成功创建了一个MyComponent
对象,但是其create
方法还未执行,所以myWindow
字段为 null,这里可能有小伙伴会问了,myComponent
对象的clickListener
字段呢?怎么不见了?其实这和我们在代码中定义clickListener
字段的形式有关,我们定义的是static OnClickListener clickListener;
,因此clickListener
是一个静态字段,其在类加载的完成的时候储存在 JVM 中内存区域的方法区
中,而创建的 Java 对象储存在 JVM 的堆内存中,两者不在同一块内存区域。关于这些细节,想深入了解的小伙伴建议阅读《深入理解JVM虚拟机》。好了,我们继续执行代码:
myComponent.create
方法执行完成之后创建了OnClickListener
内部类对象,并且为myWindow
对象设置OnCLickListener
单击事件监听。我们继续:
myComponent.destroy
方法执行完成之后,myWindow.removeClickListener
方法也执行完成,此时myWindow
对象中的clickListener
字段为 null。我们继续:
代码执行到了 80 行,在此之前,所有的代码和解释都没有什么难度,跟着运行图走,一切都那么顺利成章,其实这张图的运行结果也很好理解,只不过图中的文字需要思考一下:myComponent
引用指向的对象真的被回收了吗?要解答这个问题,我们需要借助 Java 中提供的内存分析工具jvisualvm
(以前它还不叫这个名字…),它一般在你安装 JDK 的目录下的bin
子目录下:
我们运行这个程序:
在程序左边可以找到我们当前正在执行的 Java 进程,双击进入:
单击 tab 中的监视
选项卡,可以看到当前正在执行的 Java 进程的一些资源占用信息,当然我们现在的主要目的是分析内存,那么们单击右上角的堆 Dump
:
在这个界面,单击类
选项卡,会出现当前 Java 进程中用到的所有的类,我们已经知道我们要查找的类的对象只创建了一个,因此我们根据右上角的实例数
来进行排除:我们成功的找到了我们创建的对象!而这样也意味着当我们在上面代码中调用 JVM 的垃圾回收动作没有回收这三个对象,这其实就是一个真真切切的内存泄露!因为我们将main
方法中的myComponent
引用赋值为 null,就意味着我们已经不再使用这个组件和里面的一些子组件(MyWindow
对象),即这个组件和其内部的一些组件应该被回收。但是调用 JVM 的垃圾回收却并没有将其对应的对象回收。造成这个问题的原因在哪呢?
其实就在于我们刚刚在MyComponent
类中定义的clickListener
字段,我们在代码中将其定义成了static
类型的,同时这个字段又指向了一个匿名内部类对象(在create
方法中 创建了一个OnClickListener
接口对象,即通过一个匿名内部类实现这个接口并创建其对象),根据 JVM 寻找和标记无用对象的规则(可达性分析算法),其会将clickListener
字段作为一个 “root” ,并通过它来寻找还有用的对象,在这个例子中,clickListener
字段指向一个匿名内部类对象,这个匿名内部类对象有一个外部类对象(MyComponent
类型的对象)的引用,而外部类对象中又有一个MyWindow
类型的对象引用。因此 JVM 会将这三个对象都视为有用的对象不会回收。用图来解释吧:
Ok,通过这个过程,相信你已经理解了造成此次内存泄露的原因了,那么我们该如何解决呢?对于当前这个例子,我们只需要改一些代码:
1、把MyComponent
类中的clickListener
字段前面的static
修饰符去掉就可以了(static OnClickListener clickListener;
->OnClickListener clickListener;
),这样的话clickListener
指向的对象,就作为MyComponent
类的对象的一部分了,在MyComponent
对象被回收时里面的子组件也会被回收。同时它们之间也只是互相引用(MyComponent
外部类对象中有一个指向OnClickListener
内部类对象的引用,OnClickListener
内部类对象有一个指向MyComponent
外部类对象的引用),根据 JVM 的 “可达性分析” 算法,在两个对象都不再被外部使用时,JVM 的垃圾回收机制是可以标记并回收这两个对象的。
虽然不强制要求你在MyComponent
类中的onDestroy
方法中将其clickListener
引用赋值为 null,但是我还是建议你这样做,因为这样更能确保你的程序的安全性(减少发生内存泄露的机率,毕竟匿名内部类对象会持有外部类对象的引用),在某个组件被销毁时将其内部的一些子组件进行合理的处理是一个很好的习惯。
2、你也可以自定义一个静态内部类或者是另外自定义一个类文件,并实现OnClickListener
接口,之后通过这个类创建对象,这样就可以避免通过非静态内部类的形式创建OnClickListener
对象增加内存泄露的可能性。避免内存泄漏
那么我们在日常开发中怎么合理的使用内部类来避免产生内存泄露呢?这里给出一点我个人的理解:
1、能用静态内部类就尽量使用静态内部类,从上文中我们也知道了,静态内部类的对象创建不依赖外部类对象,即静态内部对象不会持有外部类对象的引用,自然不会因为静态内部类对象而导致内存泄露,所以如果你的内部类中不需要访问外部类中的一些非static
成员,那么请把这个内部类改造成静态内部类;2、对于一些自定义类的对象,慎用
static
关键字修饰(除非这个类的对象的声明周期确实应该很长),我们已经知道,JVM 在进行垃圾回收时会将static
关键字修饰的一些静态字段作为 “root” 来进行存活对象的查找,所以程序中static
修饰的对象越多,对应的 “root” 也就越多,每一次 JVM 能回收的对象就越少。
当然这并不是建议你不使用static
关键字,只是在使用这个关键字之前可以考虑一下这个对象使用static
关键字修饰对程序的执行确实更有利吗?3、为某些组件(大型)提供一个当这个大型组件需要被回收的时候用于合理处理其中的一些小组件的方法(例如上面代码中
MyComponent
的onDestroy
方法),在这个方法中,确保正确的处理一些需要处理的对象(将某些引用置为 null、释放一些其他(CPU…)资源)。好了,关于 Java 内部类的介绍就到这里了,通过这篇文章相信你对 Java 内部类已经有了一个比较深入的理解。
如果博客中有什么不正确的地方,还请多多指点。如果这篇文章对您有帮助,请不要吝啬您的赞,欢迎继续关注我的其他文章。谢谢观看。。。
-
一文搞懂Java的 构造方法 和 访问权限
2020-07-20 16:41:54Java是一门当今最火的编程语言之一,拥有很多现成可用的库,在我们编程生涯中,有着无比重要的地位。本文主要讲解Java中的构造方法和四种类的访问权限。 -
Java 中String构造方法
2021-09-04 21:23:04API API ( Application Programming Interface,应用...Java API:指的就是JDK中提供的各种功能的Java类 这些类将底层的实现封装了起来,我们不需要关心这些类是如何实现的,只需要学习这些类如何使用即可,我们可以 -
Java 反射与内部类构造方法的问题
2018-09-06 20:33:49通过反射,我们可以获得一个类运行时的信息(属性,构造方法和普通方法),但是当我们是使用反射区获取一个类的内部类的构造方法时,会发现内部类默认的无参构造中会出现父类类型的参数 测试代码: public class ... -
java内部类最全详解
2022-03-20 10:58:56成员内部类的创建需要依赖于外部类对象-(成员方法必须通过对象调用),在没有外部类实例之前无法创建成员内部类对象 b.内部类与外部类相对独立,不是is a 的关系(发动机-汽车) c.私有属性的互相访问,内部类... -
夯实Java基础系列8:深入理解Java内部类及其实现原理
2018-04-25 23:24:10本文主要介绍了Java内部类的基本原理,使用方法和各种细节。 有关内部类实现回调,事件驱动和委托机制的文章将在后面发布。 具体代码在我的GitHub中可以找到 https://github.com/h2pl/MyTech 文章首发于我的个人... -
java-静态方法中调用构造方法
2020-06-21 20:29:37静态成员 基本定义: 静态成员包括静态方法(static method)和静态变量...java支持静态方法和静态变量,无需创建类的实例就可以调用静态方法。 ----《java语言程序设计 基础篇》原书第10版,梁勇著 要声明一个静态变量 -
java中子类调用父类构造方法,注意事项
2020-04-10 18:37:05继承有个特点,就是子类无法访问父类的private字段或者private方法。例如,Student类就无法访问Person类的name和age字段: class Person { private String name; private int age; } class Student extends ... -
Java内部类基本介绍、如何实例化内部类和如何调用外部类属性和方法
2020-06-21 19:32:50一、 Java内部类分类: 1.非静态成员内部类: 内部类没有使用static关键字修饰,并且和外部类的成员变量和方法属于同一级别,这样的是非静态成员内部类(可以使用权限修饰符修饰,和final、abstract关键字修饰)。 ... -
Java内部类和匿名内部类的用法
2016-07-21 15:46:41Java内部类和匿名内部类的用法 【尊重原创,转载请注明出处】http://blog.csdn.net/guyuealian/article/details/51981163一、内部类: (1)内部类的同名方法 内部类可以调用外部类的方法,如果内部类有同名方法... -
Java中如何在无参构造方法中调用有参构造?
2021-02-28 08:50:22有继承:处理静态动作, 分配内存空间, 变量定义为初始值 , 从基类->子类, 处理定义处的初始化, 执行构造方法 需要注意的点: 静态属性等从基类->子类进行初始化 默认无参构造方法相关 5.访问控制 这也是java... -
Java内部类的定义、如何创建内部类、内部类的分类、内部类与外部类的关系
2018-10-21 21:38:32文章目录1 内部类的基本概念1.1 内部类的定义1.2 内部类的优缺点2 创建内部类2.1 在外部类外部创建非静态内部类2.2 在外部类外部创建静态内部类...3.2 静态内部类3.3 方法内部类3.4 匿名内部类4 内部类与外部类的关系... -
java子类调用父类构造方法
2021-04-16 16:41:33在Java中,子类的构造过程中,必须调用其父类的构造函数,是因为有继承关系存在时,子类要把父类的内容继承下来,通过什么手段做到的? 答案如下: 当你new一... -
Java内部类与静态内部类的区别
2018-11-14 18:41:25在阅读Java Collection Framwork时,LinkedList中有个静态内部类: private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E&... -
Java 匿名类(匿名内部类)
2020-12-25 23:41:48Java匿名类(匿名内部类) 学习的参考博文:无恨之都 1. 初识 匿名类 标准说法: 内部类包括:成员类、局部类、匿名类(匿名内部类)。 匿名类概念: 匿名类可以使你的代码更加简洁 (JDK8之后Lambda更简洁)。 你可以... -
Java构造方法 重载与重写
2019-04-18 18:10:44内容部分引自《Java语言程序设计》一书,摘抄以便记忆和复习 1.Java中的构造方法 构造方法的特性: 必须具备和所在类相同的名字 ...一个类可以不定义构造方法,类中会隐含创建一个方法体为空的构造方法(类中... -
Java学习——构造方法和get/set()方法
2020-11-10 22:28:01构造方法(构造器):每个类都有一个默认的无参构造器,jvm自动给我们添加的, 如果我们手写一个构造器,jvm不会给我们自动添加构造器。 语法: <访问修饰符> <方法名称>(参数列表){ 实现体 } 作用:对... -
内部类可以访问外部类的属性和方法吗?
2021-02-12 19:29:53注意:在内部类构造的时候,会将外部类的引用传递进来,并且作为内部类的一个属性,所以内部类会持有一个其外部类的引用。当内部类调用外部类的私有属性时,其真正的执行是调用了编译器生成的属性的静态方法(即acess... -
【Java 内部类和异常类(详细版)】
2021-04-08 17:24:17Java支持在一个类中声明另一个类,这样的类称作内部类,而包含内部类的类成为内部类的外嵌类。 内部类的类体中不可以声明类变量和类方法。外嵌类的类体中可以用内部类声明对象,作为外嵌类的成员。 内部类的使用规则... -
java(4)-深入理解java嵌套类、内部类以及内部类builder构建构造函数
2012-06-08 09:42:26一、什么是嵌套类及内部类 可以在一个类的内部定义另一个类,这种类称为嵌套类(nested classes),它有两种类型:静态嵌套类和非静态嵌套类。静态嵌套类使用很少,最重要... 其二、在一个方法(外部类的方法)中 -
java基础之构造方法、数组
2018-10-14 15:36:05二、 构造方法和匿名对象。 1 1. 对象的产生格式: 2 2. 构造方法和普通方法的区别 2 3.构造方法的作用 2 三、数组(引用数据) 2 3.1数组的基本概念 2 3.2数组的访问 3 3.3数组的储存结构 3 3.5 数组与方法... -
java基础之构造方法私有化
2018-10-29 21:59:30构造方法私有化 构造方法私有化的操作 单例与多例设计模式 单例设计模式 (Singleton) ...当类中的构造方法私有化的时候,该类不能通过new 关键字进行实例化。那么该如何解决呢??? ... -
Java内部类(一篇就够)
2020-08-14 16:05:06Java内部类,相信大家都用过,但是多数同学可能对它了解的并不深入,只是靠记忆来完成日常工作,却不能融会贯通,遇到奇葩问题更是难以有思路去解决。这篇文章带大家一起死磕Java内部类的方方面面。 友情提示:这篇... -
java内部类的四大作用
2019-02-15 14:00:34什么是内部类:在类里面重新定义一个类,如下 public class OuterClass { private String name ; class InnerClass{ public InnerClass(){ name = &amp;quot;chenssy&amp;quot;; } } } ... -
java子类调用父类的构造方法
2021-04-16 16:45:12class FatherClass{ public FatherClass(){ System.out.println("父类 无参 构造函数"); } public FatherClass(int i){ System.out.println("父类 一个参数构造函数... -
java内部类的初始化
2011-09-08 09:51:36public class InnerClassTest { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated...静态内部类属于类本身,初始化时直接使用外部类调用静态内部类的构造方法即可。