精华内容
下载资源
问答
  • 详解 Java 内部

    千次阅读 2021-03-06 18:10:33
    内部在 Java 里面算是非常常见的一个功能了,在日常开发中我们...我们来一个个看:普通内部这个是最常见的内部之一了,其定义也很简单,在一个里面作为的一个字段直接定义就可以了,:publicclassInne...

    内部类在 Java 里面算是非常常见的一个功能了,在日常开发中我们肯定多多少少都用过,这里总结一下关于 Java 中内部类的相关知识点和一些使用内部类时需要注意的点。

    从种类上说,内部类可以分为四类:普通内部类、静态内部类、匿名内部类、局部内部类。我们来一个个看:

    普通内部类

    这个是最常见的内部类之一了,其定义也很简单,在一个类里面作为类的一个字段直接定义就可以了,例:

    public class InnerClassTest {

    public class InnerClassA {

    }

    }

    在这里 InnerClassA 类为 InnerClassTest 类的普通内部类,在这种定义方式下,普通内部类对象依赖外部类对象而存在,即在创建一个普通内部类对象时首先需要创建其外部类对象,我们在创建上面代码中的 InnerClassA 对象时先要创建 InnerClassTest 对象,例:

    public class InnerClassTest {

    public int field1 = 1;

    protected int field2 = 2;

    int field3 = 3;

    private int field4 = 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 = 1;

    protected int field2 = 2;

    int field3 = 3;

    private int field4 = 4;

    //        static int field5 = 5; // 编译错误!普通内部类中不能定义 static 属性

    public InnerClassA() {

    System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");

    System.out.println("其外部类的 field1 字段的值为: " + field1);

    System.out.println("其外部类的 field2 字段的值为: " + field2);

    System.out.println("其外部类的 field3 字段的值为: " + field3);

    System.out.println("其外部类的 field4 字段的值为: " + field4);

    }

    }

    public static void main(String[] args) {

    InnerClassTest outerObj = new InnerClassTest();

    // 不在外部类内部,使用:外部类对象. new 内部类构造器(); 的方式创建内部类对象

    //        InnerClassA innerObj = outerObj.new InnerClassA();

    }

    }

    这里的内部类就像外部类声明的一个属性字段一样,因此其的对象时依附于外部类对象而存在的,我们来看一下结果:

    2c2d58ac620e396478f7e4caed6931c9.png

    我们注意到,内部类对象可以访问外部类对象中所有访问权限的字段,同时,外部类对象也可以通过内部类的对象引用来访问内部类中定义的所有访问权限的字段,后面我们将从源码里面分析具体的原因。

    我们下面来看一下静态内部类。

    静态内部类

    我们知道,一个类的静态成员独立于这个类的任何一个对象存在,只要在具有访问权限的地方,我们就可以通过 类名.静态成员名 的形式来访问这个静态成员,同样的,静态内部类也是作为一个外部类的静态成员而存在,创建一个类的静态内部类对象不需要依赖其外部类对象。例:

    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();

    }

    }

    结果:

    5d32bf27f357287be3dbe0e7a97d315d.png

    可以看到,静态内部类就像外部类的一个静态成员一样,创建其对象无需依赖外部类对象(访问一个类的静态成员也无需依赖这个类的对象,因为它是独立于所有类的对象的)。但是于此同时,静态内部类中也无法访问外部类的非静态成员,因为外部类的非静态成员是属于每一个外部类对象的,而本身静态内部类就是独立外部类对象存在的,所以静态内部类不能访问外部类的非静态成员,而外部类依然可以访问静态内部类对象的所有访问权限的成员,这一点和普通内部类无异。

    匿名内部类

    匿名内部类有多种形式,其中最常见的一种形式莫过于在方法参数中新建一个接口对象 / 类对象,并且实现这个接口声明 / 类中原有的方法了:

    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();

    }

    }

    来看看结果:

    b70bab7679679fe17d6c8f94e9387e84.png

    上面的代码中展示了常见的两种使用匿名内部类的情况:

    直接 new 一个接口,并实现这个接口声明的方法,在这个过程其实会创建一个匿名内部类实现这个接口,并重写接口声明的方法,然后再创建一个这个匿名内部类的对象并赋值给前面的 OnClickListener 类型的引用;

    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();

    }

    }

    同样的,在局部内部类里面可以访问外部类对象的所有访问权限的字段,而外部类却不能访问局部内部类中定义的字段,因为局部内部类的定义只在其特定的方法体 / 代码块中有效,一旦出了这个定义域,那么其定义就失效了,就像代码注释中描述的那样,即外部类不能获取局部内部类的对象,因而无法访问局部内部类的字段。最后看看运行结果:

    585b22d4371880ec74d2456d82e2e95c.png

    内部类的嵌套

    内部类的嵌套,即为内部类中再定义内部类,这个问题从内部类的分类角度去考虑比较合适:

    普通内部类:在这里我们可以把它看成一个外部类的普通成员方法,在其内部可以定义普通内部类(嵌套的普通内部类),但是无法定义 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 的字节码如下:

    0536fbd5a3007a497d7f5aeeed304d59.png

    我们注意到字节码中多了一个默认修饰权限并且名为 access$100 的静态方法,其接受一个 InnerClassTest 类型的参数,即其接受一个外部类对象作为参数,方法内部用三条指令取到参数对象的 field2 字段的值并返回。由此,我们现在大概能猜到内部类对象是怎么取到外部类的 private 权限的字段了:就是通过这个外部类提供的静态方法。

    类似的,我们注意到 24 行字节码指令 invokestatic ,这里代表执行了一个静态方法,而后面的注释也写的很清楚,调用的是 InnerClassTest$InnerClassA.access$000 方法,即调用了内部类中名为 access$000 的静态方法,根据我们上面的外部类字节码规律,我们也能猜到这个方法就是内部类编译过程中编译器自动生成的,那么我们赶紧来看一下 InnerClassTest$InnerClassA 类的字节码吧:

    703e267a92cae5e46b9d559db93e06b7.png

    果然,我们在这里发现了名为 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:

    cc910d8a657110ab1b7b485178a577bb.png

    仔细看一下,确实没有找到指向外部类对象的引用,编译器只为这个静态内部类提供了一个无参构造方法。

    而且因为外部类对象需要访问当前类的私有成员,编译器给这个静态内部类生成了一个名为 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 

    int main() {

    // 申请一段内存,空间为 100 个 int 元素所占的字节数

    int *p = new int[100];

    // C++ 11

    p = nullptr;

    return 0;

    }

    在这段代码里我有意而为之:在为指针 p 申请完内存之后将其直接赋值为 nullptr ,这是 C++ 11 中一个表示空指针的关键字,我们平时常用的 NULL 只是一个值为 0 的常量值,在进行方法重载传参的时候可能会引起混淆。之后我直接返回了,虽然在程序结束之后操作系统会回收我们程序中申请的内存,但是不可否认的是上面的代码确实产生了内存泄露(申请的 100 个 int 元素所占的内存无法被回收)。这只是一个最简单不过的例子。我们在写这类程序的时候当动态申请的内存不再使用时,应该要主动释放申请的内存:

    #include 

    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 模式查看:

    ca4e738bdf06a487f69e01df6a9aa0b9.png

    程序执行到 72 行代码,此时 72 行代码还未执行,因此 myComponent 引用和其对象还未创建,继续执行:

    e3b22d959981e90b683cf92f7581522c.png

    这里成功创建了一个 MyComponent 对象,但是其 create 方法还未执行,所以 myWindow 字段为 null,这里可能有小伙伴会问了,myComponent 对象的 clickListener 字段呢?怎么不见了?其实这和我们在代码中定义 clickListener 字段的形式有关,我们定义的是 static OnClickListener clickListener; ,因此 clickListener 是一个静态字段,其在类加载的完成的时候储存在 JVM 中内存区域的 方法区 中,而创建的 Java 对象储存在 JVM 的堆内存中,两者不在同一块内存区域。关于这些细节,想深入了解的小伙伴建议阅读《深入理解JVM虚拟机》。好了,我们继续执行代码:

    13d6856e38645a15f3de483d31fb5d11.png

    myComponent.create 方法执行完成之后创建了 OnClickListener 内部类对象,并且为 myWindow 对象设置 OnCLickListener 单击事件监听。我们继续:

    a773b0fd12a1e56645ae6100d78e82ee.png

    myComponent.destroy 方法执行完成之后,myWindow.removeClickListener 方法也执行完成,此时 myWindow 对象中的 clickListener字段为 null。我们继续:

    c77f768f9b694ae7ba6c0aa7e8d1c929.png

    代码执行到了 80 行,在此之前,所有的代码和解释都没有什么难度,跟着运行图走,一切都那么顺利成章,其实这张图的运行结果也很好理解,只不过图中的文字需要思考一下:myComponent 引用指向的对象真的被回收了吗?要解答这个问题,我们需要借助 Java 中提供的内存分析工具 jvisualvm (以前它还不叫这个名字…),它一般在你安装 JDK 的目录下的 bin 子目录下:

    5ae9d9ca57aa4445b6910b4d5015ff45.png

    我们运行这个程序:

    e743560a0bfe456f756fb3622c7856d3.png

    在程序左边可以找到我们当前正在执行的 Java 进程,双击进入:

    30bdca25e88dddd665240d30eab58b0f.png

    单击 tab 中的 监视 选项卡,可以看到当前正在执行的 Java 进程的一些资源占用信息,当然我们现在的主要目的是分析内存,那么们单击右上角的 堆 Dump :

    198397ce1aaa7106ea5ff0e811d04d11.png

    在这个界面,单击 类 选项卡,会出现当前 Java 进程中用到的所有的类,我们已经知道我们要查找的类的对象只创建了一个,因此我们根据右上角的 实例数 来进行排除:我们成功的找到了我们创建的对象!而这样也意味着当我们在上面代码中调用 JVM 的垃圾回收动作没有回收这三个对象,这其实就是一个真真切切的内存泄露!因为我们将 main 方法中的 myComponent 引用赋值为 null,就意味着我们已经不再使用这个组件和里面的一些子组件(MyWindow 对象),即这个组件和其内部的一些组件应该被回收。但是调用 JVM 的垃圾回收却并没有将其对应的对象回收。造成这个问题的原因在哪呢?

    其实就在于我们刚刚在 MyComponent 类中定义的 clickListener 字段,我们在代码中将其定义成了 static 类型的,同时这个字段又指向了一个匿名内部类对象(在 create 方法中 创建了一个 OnClickListener 接口对象,即通过一个匿名内部类实现这个接口并创建其对象),根据 JVM 寻找和标记无用对象的规则(可达性分析算法),其会将 clickListener 字段作为一个 “root” ,并通过它来寻找还有用的对象,在这个例子中,clickListener 字段指向一个匿名内部类对象,这个匿名内部类对象有一个外部类对象(MyComponent 类型的对象)的引用,而外部类对象中又有一个 MyWindow 类型的对象引用。因此 JVM 会将这三个对象都视为有用的对象不会回收。用图来解释吧:

    a5979db4dbf3bebfda98d5adf914b591.png

    Ok,通过这个过程,相信你已经理解了造成此次内存泄露的原因了,那么我们该如何解决呢?对于当前这个例子,我们只需要改一些代码:

    把 MyComponent 类中的 clickListener 字段前面的 static 修饰符去掉就可以了(static OnClickListener clickListener; -> OnClickListener clickListener;),这样的话 clickListener 指向的对象,就作为 MyComponent 类的对象的一部分了,在 MyComponent 对象被回收时里面的子组件也会被回收。同时它们之间也只是互相引用(MyComponent 外部类对象中有一个指向 OnClickListener 内部类对象的引用,OnClickListener 内部类对象有一个指向 MyComponent 外部类对象的引用),根据 JVM 的 “可达性分析” 算法,在两个对象都不再被外部使用时,JVM 的垃圾回收机制是可以标记并回收这两个对象的。 虽然不强制要求你在 MyComponent 类中的 onDestroy 方法中将其 clickListener 引用赋值为 null,但是我还是建议你这样做,因为这样更能确保你的程序的安全性(减少发生内存泄露的机率,毕竟匿名内部类对象会持有外部类对象的引用),在某个组件被销毁时将其内部的一些子组件进行合理的处理是一个很好的习惯。

    你也可以自定义一个静态内部类或者是另外自定义一个类文件,并实现 OnClickListener 接口,之后通过这个类创建对象,这样就可以避免通过非静态内部类的形式创建 OnClickListener 对象增加内存泄露的可能性。

    避免内存泄漏

    那么我们在日常开发中怎么合理的使用内部类来避免产生内存泄露呢?这里给出一点我个人的理解:

    能用静态内部类就尽量使用静态内部类,从上文中我们也知道了,静态内部类的对象创建不依赖外部类对象,即静态内部对象不会持有外部类对象的引用,自然不会因为静态内部类对象而导致内存泄露,所以如果你的内部类中不需要访问外部类中的一些非 static 成员,那么请把这个内部类改造成静态内部类;

    对于一些自定义类的对象,慎用 static 关键字修饰(除非这个类的对象的声明周期确实应该很长),我们已经知道,JVM 在进行垃圾回收时会将 static 关键字修饰的一些静态字段作为 “root” 来进行存活对象的查找,所以程序中 static 修饰的对象越多,对应的 “root” 也就越多,每一次 JVM 能回收的对象就越少。 当然这并不是建议你不使用 static 关键字,只是在使用这个关键字之前可以考虑一下这个对象使用 static 关键字修饰对程序的执行确实更有利吗?

    为某些组件(大型)提供一个当这个大型组件需要被回收的时候用于合理处理其中的一些小组件的方法(例如上面代码中 MyComponent 的 onDestroy 方法),在这个方法中,确保正确的处理一些需要处理的对象(将某些引用置为 null、释放一些其他(CPU…)资源)

    展开全文
  • 在实例化一个对象时,JVM首先会检查相关类型是否已经加载并初始化,如果没有,则JVM立即进行加载并调用构造器完成的初始化。在初始化过程中或初始化完毕后,根据具体情况才会去对进行实例化。本文试图对JVM...

    摘要:

    在Java中,一个对象在可以被使用之前必须要被正确地初始化,这一点是Java规范规定的。在实例化一个对象时,JVM首先会检查相关类型是否已经加载并初始化,如果没有,则JVM立即进行加载并调用类构造器完成类的初始化。在类初始化过程中或初始化完毕后,根据具体情况才会去对类进行实例化。本文试图对JVM执行类初始化和实例化的过程做一个详细深入地介绍,以便从Java虚拟机的角度清晰解剖一个Java对象的创建过程。

    版权声明:

    友情提示:

    一个Java对象的创建过程往往包括类初始化 和 类实例化 两个阶段。本文的姊妹篇《 JVM类加载机制概述:加载时机与加载过程》主要介绍了类的初始化时机和初始化过程,本文在此基础上,进一步阐述了一个Java对象创建的真实过程。

    一、Java对象创建时机

    我们知道,一个对象在可以被使用之前必须要被正确地实例化。在Java代码中,有很多行为可以引起对象的创建,最为直观的一种就是使用new关键字来调用一个类的构造函数显式地创建对象,这种方式在Java规范中被称为 : 由执行类实例创建表达式而引起的对象创建。除此之外,我们还可以使用反射机制(Class类的newInstance方法、使用Constructor类的newInstance方法)、使用Clone方法、使用反序列化等方式创建对象。下面笔者分别对此进行一一介绍:

    1). 使用new关键字创建对象

    这是我们最常见的也是最简单的创建对象的方式,通过这种方式我们可以调用任意的构造函数(无参的和有参的)去创建对象。比如:

    Student student = new Student();

    2). 使用Class类的newInstance方法(反射机制)

    我们也可以通过Java的反射机制使用Class类的newInstance方法来创建对象,事实上,这个newInstance方法调用无参的构造器创建对象,比如:

    Student student2 = (Student)Class.forName("Student类全限定名").newInstance();

    或者:

    Student stu = Student.class.newInstance();

    3). 使用Constructor类的newInstance方法(反射机制)

    java.lang.relect.Constructor类里也有一个newInstance方法可以创建对象,该方法和Class类中的newInstance方法很像,但是相比之下,Constructor类的newInstance方法更加强大些,我们可以通过这个newInstance方法调用有参数的和私有的构造函数,比如:

    public class Student {

    private int id;

    public Student(Integer id) {

    this.id = id;

    }

    public static void main(String[] args) throws Exception {

    Constructor constructor = Student.class

    .getConstructor(Integer.class);

    Student stu3 = constructor.newInstance(123);

    }

    }

    使用newInstance方法的这两种方式创建对象使用的就是Java的反射机制,事实上Class的newInstance方法内部调用的也是Constructor的newInstance方法。

    4). 使用Clone方法创建对象

    无论何时我们调用一个对象的clone方法,JVM都会帮我们创建一个新的、一样的对象,特别需要说明的是,用clone方法创建对象的过程中并不会调用任何构造函数。关于如何使用clone方法以及浅克隆/深克隆机制,笔者已经在博文《 Java String 综述(下篇)》做了详细的说明。简单而言,要想使用clone方法,我们就必须先实现Cloneable接口并实现其定义的clone方法,这也是原型模式的应用。比如:

    public class Student implements Cloneable{

    private int id;

    public Student(Integer id) {

    this.id = id;

    }

    @Override

    protected Object clone() throws CloneNotSupportedException {

    // TODO Auto-generated method stub

    return super.clone();

    }

    public static void main(String[] args) throws Exception {

    Constructor constructor = Student.class

    .getConstructor(Integer.class);

    Student stu3 = constructor.newInstance(123);

    Student stu4 = (Student) stu3.clone();

    }

    }

    5). 使用(反)序列化机制创建对象

    当我们反序列化一个对象时,JVM会给我们创建一个单独的对象,在此过程中,JVM并不会调用任何构造函数。为了反序列化一个对象,我们需要让我们的类实现Serializable接口,比如:

    public class Student implements Cloneable, Serializable {

    private int id;

    public Student(Integer id) {

    this.id = id;

    }

    @Override

    public String toString() {

    return "Student [id=" + id + "]";

    }

    public static void main(String[] args) throws Exception {

    Constructor constructor = Student.class

    .getConstructor(Integer.class);

    Student stu3 = constructor.newInstance(123);

    // 写对象

    ObjectOutputStream output = new ObjectOutputStream(

    new FileOutputStream("student.bin"));

    output.writeObject(stu3);

    output.close();

    // 读对象

    ObjectInputStream input = new ObjectInputStream(new FileInputStream(

    "student.bin"));

    Student stu5 = (Student) input.readObject();

    System.out.println(stu5);

    }

    }

    6). 完整实例

    public class Student implements Cloneable, Serializable {

    private int id;

    public Student() {

    }

    public Student(Integer id) {

    this.id = id;

    }

    @Override

    protected Object clone() throws CloneNotSupportedException {

    // TODO Auto-generated method stub

    return super.clone();

    }

    @Override

    public String toString() {

    return "Student [id=" + id + "]";

    }

    public static void main(String[] args) throws Exception {

    System.out.println("使用new关键字创建对象:");

    Student stu1 = new Student(123);

    System.out.println(stu1);

    System.out.println("\n---------------------------\n");

    System.out.println("使用Class类的newInstance方法创建对象:");

    Student stu2 = Student.class.newInstance(); //对应类必须具有无参构造方法,且只有这一种创建方式

    System.out.println(stu2);

    System.out.println("\n---------------------------\n");

    System.out.println("使用Constructor类的newInstance方法创建对象:");

    Constructor constructor = Student.class

    .getConstructor(Integer.class); // 调用有参构造方法

    Student stu3 = constructor.newInstance(123);

    System.out.println(stu3);

    System.out.println("\n---------------------------\n");

    System.out.println("使用Clone方法创建对象:");

    Student stu4 = (Student) stu3.clone();

    System.out.println(stu4);

    System.out.println("\n---------------------------\n");

    System.out.println("使用(反)序列化机制创建对象:");

    // 写对象

    ObjectOutputStream output = new ObjectOutputStream(

    new FileOutputStream("student.bin"));

    output.writeObject(stu4);

    output.close();

    // 读取对象

    ObjectInputStream input = new ObjectInputStream(new FileInputStream(

    "student.bin"));

    Student stu5 = (Student) input.readObject();

    System.out.println(stu5);

    }

    }/* Output:

    使用new关键字创建对象:

    Student [id=123]

    ---------------------------

    使用Class类的newInstance方法创建对象:

    Student [id=0]

    ---------------------------

    使用Constructor类的newInstance方法创建对象:

    Student [id=123]

    ---------------------------

    使用Clone方法创建对象:

    Student [id=123]

    ---------------------------

    使用(反)序列化机制创建对象:

    Student [id=123]

    *///:~

    从Java虚拟机层面看,除了使用new关键字创建对象的方式外,其他方式全部都是通过转变为invokevirtual指令直接创建对象的。

    二. Java 对象的创建过程

    当一个对象被创建时,虚拟机就会为其分配内存来存放对象自己的实例变量及其从父类继承过来的实例变量(即使这些从超类继承过来的实例变量有可能被隐藏也会被分配空间)。在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值(零值)。在内存分配完成之后,Java虚拟机就会开始对新创建的对象按照程序猿的意志进行初始化。在Java对象初始化过程中,主要涉及三种执行对象初始化的结构,分别是实例变量初始化、实例代码块初始化以及 构造函数初始化。

    1、实例变量初始化与实例代码块初始化

    我们在定义(声明)实例变量的同时,还可以直接对实例变量进行赋值或者使用实例代码块对其进行赋值。如果我们以这两种方式为实例变量进行初始化,那么它们将在构造函数执行之前完成这些初始化操作。实际上,如果我们对实例变量直接赋值或者使用实例代码块赋值,那么编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后(还记得吗?Java要求构造函数的第一条语句必须是超类构造函数的调用语句),构造函数本身的代码之前。例如:

    public class InstanceVariableInitializer {

    private int i = 1;

    private int j = i + 1;

    public InstanceVariableInitializer(int var){

    System.out.println(i);

    System.out.println(j);

    this.i = var;

    System.out.println(i);

    System.out.println(j);

    }

    { // 实例代码块

    j += 3;

    }

    public static void main(String[] args) {

    new InstanceVariableInitializer(8);

    }

    }/* Output:

    1

    5

    8

    5

    *///:~

    上面的例子正好印证了上面的结论。特别需要注意的是,Java是按照编程顺序来执行实例变量初始化器和实例初始化器中的代码的,并且不允许顺序靠前的实例代码块初始化在其后面定义的实例变量,比如:

    public class InstanceInitializer {

    {

    j = i;

    }

    private int i = 1;

    private int j;

    }

    public class InstanceInitializer {

    private int j = i;

    private int i = 1;

    }

    上面的这些代码都是无法通过编译的,编译器会抱怨说我们使用了一个未经定义的变量。之所以要这么做是为了保证一个变量在被使用之前已经被正确地初始化。但是我们仍然有办法绕过这种检查,比如:

    public class InstanceInitializer {

    private int j = getI();

    private int i = 1;

    public InstanceInitializer() {

    i = 2;

    }

    private int getI() {

    return i;

    }

    public static void main(String[] args) {

    InstanceInitializer ii = new InstanceInitializer();

    System.out.println(ii.j);

    }

    }

    如果我们执行上面这段代码,那么会发现打印的结果是0。因此我们可以确信,变量j被赋予了i的默认值0,这一动作发生在实例变量i初始化之前和构造函数调用之前。

    2、构造函数初始化

    我们可以从上文知道,实例变量初始化与实例代码块初始化总是发生在构造函数初始化之前,那么我们下面着重看看构造函数初始化过程。众所周知,每一个Java中的对象都至少会有一个构造函数,如果我们没有显式定义构造函数,那么它将会有一个默认无参的构造函数。在编译生成的字节码中,这些构造函数会被命名成()方法,参数列表与Java语言书写的构造函数的参数列表相同。

    我们知道,Java要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性。事实上,这一点是在构造函数中保证的:Java强制要求Object对象(Object是Java的顶层对象,没有超类)之外的所有对象构造函数的第一条语句必须是超类构造函数的调用语句或者是类中定义的其他的构造函数,如果我们既没有调用其他的构造函数,也没有显式调用超类的构造函数,那么编译器会为我们自动生成一个对超类构造函数的调用,比如:

    public class ConstructorExample {

    }

    对于上面代码中定义的类,我们观察编译之后的字节码,我们会发现编译器为我们生成一个构造函数,如下,

    aload_0

    invokespecial #8; //Method java/lang/Object."":()V

    return

    上面代码的第二行就是调用Object类的默认构造函数的指令。也就是说,如果我们显式调用超类的构造函数,那么该调用必须放在构造函数所有代码的最前面,也就是必须是构造函数的第一条指令。正因为如此,Java才可以使得一个对象在初始化之前其所有的超类都被初始化完成,并保证创建一个完整的对象出来。

    特别地,如果我们在一个构造函数中调用另外一个构造函数,如下所示,

    public class ConstructorExample {

    private int i;

    ConstructorExample() {

    this(1);

    ....

    }

    ConstructorExample(int i) {

    ....

    this.i = i;

    ....

    }

    }

    对于这种情况,Java只允许在ConstructorExample(int i)内调用超类的构造函数,也就是说,下面两种情形的代码编译是无法通过的:

    public class ConstructorExample {

    private int i;

    ConstructorExample() {

    super();

    this(1); // Error:Constructor call must be the first statement in a constructor

    ....

    }

    ConstructorExample(int i) {

    ....

    this.i = i;

    ....

    }

    }

    或者,

    public class ConstructorExample {

    private int i;

    ConstructorExample() {

    this(1);

    super(); //Error: Constructor call must be the first statement in a constructor

    ....

    }

    ConstructorExample(int i) {

    this.i = i;

    }

    }

    Java通过对构造函数作出这种限制以便保证一个类的实例能够在被使用之前正确地初始化。

    3、 小结

    总而言之,实例化一个类的对象的过程是一个典型的递归过程,如下图所示。进一步地说,在实例化一个类的对象时,具体过程是这样的:

    在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到Object类。此时,首先实例化Object类,再依次对以下各类进行实例化,直到完成对目标类的实例化。具体而言,在实例化每个类时,都遵循如下顺序:先依次执行实例变量初始化和实例代码块初始化,再执行构造函数初始化。也就是说,编译器会将实例变量初始化和实例代码块初始化相关代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后,构造函数本身的代码之前。

    cb3d3530c381745b11ae7e896f129dff.png

    Ps: 关于递归的思想与内涵的介绍,请参见我的博文《 算法设计方法:递归的内涵与经典应用》。

    4、实例变量初始化、实例代码块初始化以及构造函数初始化综合实例

    笔者在《 JVM类加载机制概述:加载时机与加载过程》一文中详细阐述了类初始化时机和初始化过程,并在文章的最后留了一个悬念给各位,这里来揭开这个悬念。建议读者先看完《 JVM类加载机制概述:加载时机与加载过程》这篇再来看这个,印象会比较深刻,如若不然,也没什么关系~~

    //父类

    class Foo {

    int i = 1;

    Foo() {

    System.out.println(i); -----------(1)

    int x = getValue();

    System.out.println(x); -----------(2)

    }

    {

    i = 2;

    }

    protected int getValue() {

    return i;

    }

    }

    //子类

    class Bar extends Foo {

    int j = 1;

    Bar() {

    j = 2;

    }

    {

    j = 3;

    }

    @Override

    protected int getValue() {

    return j;

    }

    }

    public class ConstructorExample {

    public static void main(String... args) {

    Bar bar = new Bar();

    System.out.println(bar.getValue()); -----------(3)

    }

    }/* Output:

    2

    0

    2

    *///:~

    根据上文所述的类实例化过程,我们可以将Foo类的构造函数和Bar类的构造函数等价地分别变为如下形式:

    //Foo类构造函数的等价变换:

    Foo() {

    i = 1;

    i = 2;

    System.out.println(i);

    int x = getValue();

    System.out.println(x);

    }

    //Bar类构造函数的等价变换

    Bar() {

    Foo();

    j = 1;

    j = 3;

    j = 2

    }

    这样程序就好看多了,我们一眼就可以观察出程序的输出结果。在通过使用Bar类的构造方法new一个Bar类的实例时,首先会调用Foo类构造函数,因此(1)处输出是2,这从Foo类构造函数的等价变换中可以直接看出。(2)处输出是0,为什么呢?因为在执行Foo的构造函数的过程中,由于Bar重载了Foo中的getValue方法,所以根据Java的多态特性可以知道,其调用的getValue方法是被Bar重载的那个getValue方法。但由于这时Bar的构造函数还没有被执行,因此此时j的值还是默认值0,因此(2)处输出是0。最后,在执行(3)处的代码时,由于bar对象已经创建完成,所以此时再访问j的值时,就得到了其初始化后的值2,这一点可以从Bar类构造函数的等价变换中直接看出。

    三. 类的初始化时机与过程

    关于类的初始化时机,笔者在博文《 JVM类加载机制概述:加载时机与加载过程》已经介绍的很清楚了,此处不再赘述。简单地说,在类加载过程中,准备阶段是正式为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,而初始化阶段是真正开始执行类中定义的java程序代码(字节码)并按程序猿的意图去初始化类变量的过程。更直接地说,初始化阶段就是执行类构造器()方法的过程。()方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块static{}中的语句合并产生的,其中编译器收集的顺序是由语句在源文件中出现的顺序所决定。

    类构造器()与实例构造器()不同,它不需要程序员进行显式调用,虚拟机会保证在子类类构造器()执行之前,父类的类构造()执行完毕。由于父类的构造器()先执行,也就意味着父类中定义的静态代码块/静态变量的初始化要优先于子类的静态代码块/静态变量的初始化执行。特别地,类构造器()对于类或者接口来说并不是必需的,如果一个类中没有静态代码块,也没有对类变量的赋值操作,那么编译器可以不为这个类生产类构造器()。此外,在同一个类加载器下,一个类只会被初始化一次,但是一个类可以任意地实例化对象。也就是说,在一个类的生命周期中,类构造器()最多会被虚拟机调用一次,而实例构造器()则会被虚拟机调用多次,只要程序员还在创建对象。

    注意,这里所谓的实例构造器()是指收集类中的所有实例变量的赋值动作、实例代码块和构造函数合并产生的,类似于上文对Foo类的构造函数和Bar类的构造函数做的等价变换。

    四. 总结

    1、一个实例变量在对象初始化的过程中会被赋值几次?

    我们知道,JVM在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个时候实例变量被第一次赋值,这个赋值过程是没有办法避免的。如果我们在声明实例变量x的同时对其进行了赋值操作,那么这个时候,这个实例变量就被第二次赋值了。如果我们在实例代码块中,又对变量x做了初始化操作,那么这个时候,这个实例变量就被第三次赋值了。如果我们在构造函数中,也对变量x做了初始化操作,那么这个时候,变量x就被第四次赋值。也就是说,在Java的对象初始化过程中,一个实例变量最多可以被初始化4次。

    2、类的初始化过程与类的实例化过程的异同?

    类的初始化是指类加载过程中的初始化阶段对类变量按照程序猿的意图进行赋值的过程;而类的实例化是指在类完全加载到内存中后创建对象的过程。

    3、假如一个类还未加载到内存中,那么在创建一个该类的实例时,具体过程是怎样的?

    我们知道,要想创建一个类的实例,必须先将该类加载到内存并进行初始化,也就是说,类初始化操作是在类实例化操作之前进行的,但并不意味着:只有类初始化操作结束后才能进行类实例化操作。例如,笔者在博文《 JVM类加载机制概述:加载时机与加载过程》中所提到的下面这个经典案例:

    public class StaticTest {

    public static void main(String[] args) {

    staticFunction();

    }

    static StaticTest st = new StaticTest();

    static { //静态代码块

    System.out.println("1");

    }

    { // 实例代码块

    System.out.println("2");

    }

    StaticTest() { // 实例构造器

    System.out.println("3");

    System.out.println("a=" + a + ",b=" + b);

    }

    public static void staticFunction() { // 静态方法

    System.out.println("4");

    }

    int a = 110; // 实例变量

    static int b = 112; // 静态变量

    }/* Output:

    2

    3

    a=110,b=0

    1

    4

    *///:~

    大家能得到正确答案吗?笔者已经在博文《 JVM类加载机制概述:加载时机与加载过程》中解释过这个问题了,此不赘述。

    总的来说,类实例化的一般过程是:父类的类构造器() -> 子类的类构造器() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数。

    五. 更多

    更多关于类初始化时机和初始化过程的介绍,请参见我的博文《 JVM类加载机制概述:加载时机与加载过程》。

    更多关于类加载器等方面的内容,包括JVM预定义的类加载器、双亲委派模型等知识点,请参见我的转载博文《深入理解Java类加载器(一):Java类加载原理解析》。

    关于递归的思想与内涵的介绍,请参见我的博文《 算法设计方法:递归的内涵与经典应用》。

    展开全文
  • SENet实战详解:使用SE-ReSNet50实现对植物幼苗的分类

    千次阅读 多人点赞 2021-10-21 18:49:12
    3、详细的计算过程 首先 F t r F_{tr} Ftr​这一步是转换操作(严格讲并不属于SENet,而是属于原网络,可以看后面SENet和InceptionResNet网络的结合),在文中就是一个标准的卷积操作而已,输入输出的定义如下...

    摘要

    1、SENet概述

    ​ Squeeze-and-Excitation Networks(简称 SENet)是 Momenta 胡杰团队(WMW)提出的新的网络结构,利用SENet,一举取得最后一届 ImageNet 2017 竞赛 Image Classification 任务的冠军,在ImageNet数据集上将top-5 error降低到2.251%,原先的最好成绩是2.991%。

    作者在文中将SENet block插入到现有的多种分类网络中,都取得了不错的效果。作者的动机是希望显式地建模特征通道之间的相互依赖关系。另外,作者并未引入新的空间维度来进行特征通道间的融合,而是采用了一种全新的「特征重标定」策略。具体来说,就是通过学习的方式来自动获取到每个特征通道的重要程度,然后依照这个重要程度去提升有用的特征并抑制对当前任务用处不大的特征。

    通俗的来说SENet的核心思想在于通过网络根据loss去学习特征权重,使得有效的feature map权重大,无效或效果小的feature map权重小的方式训练模型达到更好的结果。SE block嵌在原有的一些分类网络中不可避免地增加了一些参数和计算量,但是在效果面前还是可以接受的 。Sequeeze-and-Excitation(SE) block并不是一个完整的网络结构,而是一个子结构,可以嵌到其他分类或检测模型中。

    2、SENet 结构组成详解

    上述结构中,Squeeze 和 Excitation 是两个非常关键的操作,下面进行详细说明。

    img

    上图是SE 模块的示意图。给定一个输入 x,其特征通道数为 C ′ {C}' C,通过一系列卷积等一般变换后得到一个特征通道数为C 的特征。通过下面的三个操作还重标前面得到的特征:

    1、Squeeze 操作,顺着空间维度来进行特征压缩,将每个二维的特征通道变成一个实数,这个实数某种程度上具有全局的感受野,并且输出的维度和输入的特征通道数相匹配。它表征着在特征通道上响应的全局分布,而且使得靠近输入的层也可以获得全局的感受野,这一点在很多任务中都是非常有用的。

    2、 Excitation 操作,它是一个类似于循环神经网络中门的机制。通过参数 w 来为每个特征通道生成权重,其中参数 w 被学习用来显式地建模特征通道间的相关性。

    3、 Reweight 操作,将 Excitation 的输出的权重看做是进过特征选择后的每个特征通道的重要性,然后通过乘法逐通道加权到先前的特征上,完成在通道维度上的对原始特征的重标定。

    3、详细的计算过程

    首先 F t r F_{tr} Ftr这一步是转换操作(严格讲并不属于SENet,而是属于原网络,可以看后面SENet和Inception及ResNet网络的结合),在文中就是一个标准的卷积操作而已,输入输出的定义如下表示:

    img

    那么这个 F t r F_{tr} Ftr的公式就是下面的公式1(卷积操作, V c V_{c} Vc表示第c个卷积核, X s X^{s} Xs表示第s个输入)。

    img
    F t r F_{tr} Ftr得到的U就是Figure1中的左边第二个三维矩阵,也叫tensor,或者叫C个大小为HW的feature map。而uc表示U中第c个二维矩阵,下标c表示channel。
    接下来就是Squeeze操作,公式非常简单,就是一个global average pooling:
    img
    因此公式2就将的 H × W × C H \times W \times C H×W×C输入转换成 1 × 1 × C 1 \times 1 \times C 1×1×C的输出,对应Figure1中的Fsq操作。为什么会有这一步呢?这一步的结果相当于表明该层C个feature map的数值分布情况,或者叫全局信息。
    再接下来就是Excitation操作,如公式3。直接看最后一个等号,前面squeeze得到的结果是z,这里先用W1乘以z,就是一个全连接层操作,W1的维度是 C / r × C C/ r \times C C/r×C,这个r是一个缩放参数,在文中取的是16,这个参数的目的是为了减少channel个数从而降低计算量。又因为z的维度是 1 × 1 × C 1 \times 1\times C 1×1×C所以W1z的结果就是 1 × 1 × C / r 1 \times 1 \times C / r 1×1×C/r;然后再经过一个ReLU层,输出的维度不变;然后再和W2相乘,和W2相乘也是一个全连接层的过程,W2的维度是 C × C / r C \times C/r C×C/r,因此输出的维度就是 1 × 1 × C 1 \times 1 \times C 1×1×C;最后再经过sigmoid函数,得到s:
    img
    也就是说最后得到的这个s的维度是 1 × 1 × C 1 \times 1 \times C 1×1×C,C表示channel数目。这个s其实是本文的核心,它是用来刻画tensor U中C个feature map的权重。而且这个权重是通过前面这些全连接层和非线性层学习得到的,因此可以end-to-end训练。这两个全连接层的作用就是融合各通道的feature map信息,因为前面的squeeze都是在某个channel的feature map里面操作。
    在得到s之后,就可以对原来的tensor U操作了,就是下面的公式4。也很简单,就是channel-wise multiplication,什么意思呢? u c u_{c} uc是一个二维矩阵, s c s_{c} sc是一个数,也就是权重,因此相当于把矩 u c u_{c} uc阵中的每个值都乘以 s c s_{c} sc。对应Figure1中的Fscale。

    img

    SENet 在具体网络中应用(代码实现SE_ResNet)

    介绍完具体的公式实现,下面介绍下SE block怎么运用到具体的网络之中。

    img

    上图是将 SE 模块嵌入到 Inception 结构的一个示例。方框旁边的维度信息代表该层的输出。

    这里我们使用 global average pooling 作为 Squeeze 操作。紧接着两个 Fully Connected 层组成一个 Bottleneck 结构去建模通道间的相关性,并输出和输入特征同样数目的权重。我们首先将特征维度降低到输入的 1/16,然后经过 ReLu 激活后再通过一个 Fully Connected 层升回到原来的维度。这样做比直接用一个 Fully Connected 层的好处在于:

    1)具有更多的非线性,可以更好地拟合通道间复杂的相关性;

    2)极大地减少了参数量和计算量。然后通过一个 Sigmoid 的门获得 0~1 之间归一化的权重,最后通过一个 Scale 的操作来将归一化后的权重加权到每个通道的特征上。

    除此之外,SE 模块还可以嵌入到含有 skip-connections 的模块中。上右图是将 SE 嵌入到 ResNet 模块中的一个例子,操作过程基本和 SE-Inception 一样,只不过是在 Addition 前对分支上 Residual 的特征进行了特征重标定。如果对 Addition 后主支上的特征进行重标定,由于在主干上存在 0~1 的 scale 操作,在网络较深 BP 优化时就会在靠近输入层容易出现梯度消散的情况,导致模型难以优化。

    目前大多数的主流网络都是基于这两种类似的单元通过 repeat 方式叠加来构造的。由此可见,SE 模块可以嵌入到现在几乎所有的网络结构中。通过在原始网络结构的 building block 单元中嵌入 SE 模块,我们可以获得不同种类的 SENet。如 SE-BN-Inception、SE-ResNet、SE-ReNeXt、SE-Inception-ResNet-v2 等等。

    本例通过实现SE-ResNet,来显示如何将SE模块嵌入到ResNet网络中。SE-ResNet模型如下图:

    img

    实战详解

    1、数据集

    数据集选用植物幼苗分类,总共12类。数据集连接如下:
    链接:https://pan.baidu.com/s/1gYb-3XCZBhBoEFyj6d_kdw
    提取码:q060

    在工程的根目录新建data文件夹,获取数据集后,将trian和test解压放到data文件夹下面,如下图:

    image-20211021182712396

    2、安装库,并导入需要的库

    本项目用到pretrainedmodels,这里有seresenet的预训练模型。安装方法:

    pip install pretrainedmodels
    

    安装完成后,导入到项目中。

    import torch.optim as optim
    import torch
    import torch.nn as nn
    import torch.nn.parallel
    import torch.utils.data
    import torch.utils.data.distributed
    import torchvision.transforms as transforms
    from dataset.dataset import SeedlingData
    from torch.autograd import Variable
    import pretrainedmodels
    

    3、设置全局参数

    设置使用GPU,设置学习率、BatchSize、epoch等参数

    # 设置全局参数
    modellr = 1e-4
    BATCH_SIZE = 16
    EPOCHS = 50
    DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    

    4、数据预处理

    数据处理比较简单,没有做复杂的尝试,有兴趣的可以加入一些处理。

    # 数据预处理
    
    transform = transforms.Compose([
        transforms.Resize((224, 224)),
        transforms.ToTensor(),
        transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
    
    ])
    transform_test = transforms.Compose([
        transforms.Resize((224, 224)),
        transforms.ToTensor(),
        transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
    ])
    

    5、数据读取

    然后我们在dataset文件夹下面新建 init.py和dataset.py,在mydatasets.py文件夹写入下面的代码:

    说一下代码的核心逻辑。

    第一步 建立字典,定义类别对应的ID,用数字代替类别。

    第二步 在__init__里面编写获取图片路径的方法。测试集只有一层路径直接读取,训练集在train文件夹下面是类别文件夹,先获取到类别,再获取到具体的图片路径。然后使用sklearn中切分数据集的方法,按照7:3的比例切分训练集和验证集。

    第三步 在__getitem__方法中定义读取单个图片和类别的方法,由于图像中有位深度32位的,所以我在读取图像的时候做了转换。

    代码如下:

    # coding:utf8
    import os
    from PIL import Image
    from torch.utils import data
    from torchvision import transforms as T
    from sklearn.model_selection import train_test_split
     
    Labels = {'Black-grass': 0, 'Charlock': 1, 'Cleavers': 2, 'Common Chickweed': 3,
              'Common wheat': 4, 'Fat Hen': 5, 'Loose Silky-bent': 6, 'Maize': 7, 'Scentless Mayweed': 8,
              'Shepherds Purse': 9, 'Small-flowered Cranesbill': 10, 'Sugar beet': 11}
     
     
    class SeedlingData (data.Dataset):
     
        def __init__(self, root, transforms=None, train=True, test=False):
            """
            主要目标: 获取所有图片的地址,并根据训练,验证,测试划分数据
            """
            self.test = test
            self.transforms = transforms
     
            if self.test:
                imgs = [os.path.join(root, img) for img in os.listdir(root)]
                self.imgs = imgs
            else:
                imgs_labels = [os.path.join(root, img) for img in os.listdir(root)]
                imgs = []
                for imglable in imgs_labels:
                    for imgname in os.listdir(imglable):
                        imgpath = os.path.join(imglable, imgname)
                        imgs.append(imgpath)
                trainval_files, val_files = train_test_split(imgs, test_size=0.3, random_state=42)
                if train:
                    self.imgs = trainval_files
                else:
                    self.imgs = val_files
     
        def __getitem__(self, index):
            """
            一次返回一张图片的数据
            """
            img_path = self.imgs[index]
            img_path=img_path.replace("\\",'/')
            if self.test:
                label = -1
            else:
                labelname = img_path.split('/')[-2]
                label = Labels[labelname]
            data = Image.open(img_path).convert('RGB')
            data = self.transforms(data)
            return data, label
     
        def __len__(self):
            return len(self.imgs)
    

    然后我们在train.py调用SeedlingData读取数据 ,记着导入刚才写的dataset.py(from mydatasets import SeedlingData)

    # 读取数据
    dataset_train = SeedlingData('data/train', transforms=transform, train=True)
    dataset_test = SeedlingData("data/train", transforms=transform_test, train=False)
    # 导入数据
    train_loader = torch.utils.data.DataLoader(dataset_train, batch_size=BATCH_SIZE, shuffle=True)
    test_loader = torch.utils.data.DataLoader(dataset_test, batch_size=BATCH_SIZE, shuffle=False)
    

    6、设置模型

    • 设置loss函数为nn.CrossEntropyLoss()。
    • 设置模型为se_resnet50,修改最后一层全连接输出改为12。
    • 优化器设置为adamw。
    # 实例化模型并且移动到GPU
    criterion = nn.CrossEntropyLoss()
    model_ft = pretrainedmodels.__dict__['se_resnet50'](num_classes=1000, pretrained='imagenet')
    model_ft.fc = classifier = nn.Sequential(
        nn.Linear(2048, 512),
        nn.LeakyReLU(True),
        nn.Dropout(0.5),
        nn.Linear(512, 12),
    )
    model_ft.to(DEVICE)
    # 选择简单暴力的Adam优化器,学习率调低
    optimizer = optim.AdamW(model_ft.parameters(), lr=modellr)
    

    7、定义训练和验证函数

    def adjust_learning_rate(optimizer, epoch):
        """Sets the learning rate to the initial LR decayed by 10 every 30 epochs"""
        modellrnew = modellr * (0.1 ** (epoch // 50))
        print("lr:", modellrnew)
        for param_group in optimizer.param_groups:
            param_group['lr'] = modellrnew
    
    
    # 定义训练过程
    
    def train(model, device, train_loader, optimizer, epoch):
        model.train()
        sum_loss = 0
        total_num = len(train_loader.dataset)
        print(total_num, len(train_loader))
        for batch_idx, (data, target) in enumerate(train_loader):
            data, target = Variable(data).to(device), Variable(target).to(device)
            output = model(data)
            loss = criterion(output, target)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            print_loss = loss.data.item()
            sum_loss += print_loss
            if (batch_idx + 1) % 10 == 0:
                print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                    epoch, (batch_idx + 1) * len(data), len(train_loader.dataset),
                           100. * (batch_idx + 1) / len(train_loader), loss.item()))
        ave_loss = sum_loss / len(train_loader)
        print('epoch:{},loss:{}'.format(epoch, ave_loss))
    
    
    # 验证过程
    def val(model, device, test_loader):
        model.eval()
        test_loss = 0
        correct = 0
        total_num = len(test_loader.dataset)
        print(total_num, len(test_loader))
        with torch.no_grad():
            for data, target in test_loader:
                data, target = Variable(data).to(device), Variable(target).to(device)
                output = model(data)
                loss = criterion(output, target)
                _, pred = torch.max(output.data, 1)
                correct += torch.sum(pred == target)
                print_loss = loss.data.item()
                test_loss += print_loss
            correct = correct.data.item()
            acc = correct / total_num
            avgloss = test_loss / len(test_loader)
            print('\nVal set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
                avgloss, correct, len(test_loader.dataset), 100 * acc))
    # 训练
    for epoch in range(1, EPOCHS + 1):
        adjust_learning_rate(optimizer, epoch)
        train(model_ft, DEVICE, train_loader, optimizer, epoch)
        val(model_ft, DEVICE, test_loader)
    torch.save(model_ft, 'model.pth')
    
    

    8、测试

    我介绍两种常用的测试方式,第一种是通用的,通过自己手动加载数据集然后做预测,具体操作如下:

    测试集存放的目录如下图:

    image-20211021184219787

    第一步 定义类别,这个类别的顺序和训练时的类别顺序对应,一定不要改变顺序!!!!

    classes = ('Black-grass', 'Charlock', 'Cleavers', 'Common Chickweed',
               'Common wheat', 'Fat Hen', 'Loose Silky-bent',
               'Maize', 'Scentless Mayweed', 'Shepherds Purse', 'Small-flowered Cranesbill', 'Sugar beet')
    
    

    第二步 定义transforms,transforms和验证集的transforms一样即可,别做数据增强。

    transform_test = transforms.Compose([
             transforms.Resize((224, 224)),
            transforms.ToTensor(),
            transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
    ])
    

    第三步 加载model,并将模型放在DEVICE里。

    DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    model = torch.load("model.pth")
    model.eval()
    model.to(DEVICE)
    

    第四步 读取图片并预测图片的类别,在这里注意,读取图片用PIL库的Image。不要用cv2,transforms不支持。

    path = 'data/test/'
    testList = os.listdir(path)
    for file in testList:
        img = Image.open(path + file)
        img = transform_test(img)
        img.unsqueeze_(0)
        img = Variable(img).to(DEVICE)
        out = model(img)
        # Predict
        _, pred = torch.max(out.data, 1)
        print('Image Name:{},predict:{}'.format(file, classes[pred.data.item()]))
    

    第二种,使用自定义的Dataset读取图片。前三步同上,差别主要在第四步。读取数据的时候,使用Dataset的SeedlingData读取。

    dataset_test =SeedlingData('data/test/', transform_test,test=True)
    print(len(dataset_test))
    # 对应文件夹的label
     
    for index in range(len(dataset_test)):
        item = dataset_test[index]
        img, label = item
        img.unsqueeze_(0)
        data = Variable(img).to(DEVICE)
        output = model(data)
        _, pred = torch.max(output.data, 1)
        print('Image Name:{},predict:{}'.format(dataset_test.imgs[index], classes[pred.data.item()]))
        index += 1
    

    关注公众号,回复“senet实战”,获取代码、数据集和模型。

    展开全文
  • 超详解什么是与对象一些关键字的使用(Java)

    千次阅读 多人点赞 2021-02-15 11:13:38
    与对象(Java) 一.什么是面向对象?:是现在最为流行的软件设计与开发方法 1.它是一种模块化的设计模式 2.特点:封装性、继承性和多态性 3.那么什么是? 封装性:规定了不同级别的可见性的访问权限 继承性:派生...

    类与对象(Java)

    一、什么是面向对象?:是现在最为流行的软件设计与开发方法

    1.它是一种模块化的设计模式
    2.特点:封装性、继承性和多态性
    3.那么什么是?
    封装性:规定了不同级别的可见性的访问权限
    继承性:派生类(子类)继承了超类(父类)的所有内容,并相应的增加了一些自己新的成员
    多态性:允许程序中出现重名现象 如----方法重载/对象多态

    二、什么是类?类是由成员属性和方法组成的

    1.实际上成员属性就是变量,方法就是一些操作行为(在C里面方法叫做函数)

    class Person{           //定义一个类
    	String name;       //【成员属性】人的姓名
    	int age;		   //【成员属性】人的年龄
    	public String getname(){	//获取name的属性内容
    		return name;
    	}
    	public int getage(){	//获取age的属性内容
    		return age;
    	}
    	public void tell(){
    		System.out.println("姓名: " + name + "年龄: " + age); //输出
    	}
    }
    

    三、什么是对象?:对象表示的是一个个独立的个体,对象的所有功能必须由类定义

    1.对象需要通过关键字new来分配内存空间才能使用
    2.通过实例化对象可以进行类操作

    class Person{           //定义一个类
    	String name;       //【成员属性】人的姓名
    	int age;		   //【成员属性】人的年龄
    	public String getname(){	//获取name的属性内容
    		return name;
    	}
    	public int getage(){	//获取age的属性内容
    		return age;
    	}
    	public void tell(){ //输出函数
    		System.out.println("姓名: " + name + "年龄: " + age); 
    	}
    }
    
    public class JavaDemo{	//通过实例化对象进行类操作
    	public static void main(String args[]){
    		Person per = new Person();	//声明并实例化对象,其实也就是说给对象per分配了一块堆内存的空间来保存该类中的成员属性
    		per.name = "张三";
    		per.age = 18;
    		per.tell();
    	}
    }
    

    四、对象内存是怎样分配的?对象名称----栈内存、具体信息—堆内存

    五、引用传递?:每一块栈内存都会保存有堆内存的信息,并且只允许保存一个堆内存的地址信息

    这里有一点继承的意思在里面,通过引用传递,可以使新的对象保存旧的对象里面堆内存的信息,同时旧的对象也可以使用堆内存信息

    public class JavaDemo{	//通过实例化对象进行类操作
    	public static void main(String args[]){
    		Person per1 = new Person();	//声明并实例化对象,其实也就是说给对象per分配了一块堆内存的空间来保存该类中的成员属性
    		per1.name = "张三";
    		per1.age = 18;
    		Person per2 = per1;	//引用传递(有一点C里面赋值的意思在里面)
    		per2.age = 80;
    		per1.tell();	//通过引用传递可以改变堆内存中保存的信息,这时再输出对象per1的年龄则变为80了
    	}
    }
    

    六、什么是成员属性封装?只能通过方法来改变,封装内的属性/或实例化对象

    class Person{           //定义一个类
    	private String name;       //【成员属性】人的姓名
    	private int age;		   //【成员属性】人的年龄
    	public void setname(String tempname){	//设置name的属性
    		name = tempname;
    	}
    	public void setage(int tempage){	//设置age的属性
    		age = tempage;
    	}
    	public String getname(){	//获取name的属性内容
    		return name;
    	}
    	public int getage(){	//获取age的属性内容
    		return age;
    	}
    	public void tell(){ //输出函数
    		System.out.println("姓名: " + name + "年龄: " + age); 
    	}
    }
    
    public class JavaDemo{	//通过实例化对象进行类操作
    	public static void main(String args[]){
    		Person per = new Person();	//声明并实例化对象,其实也就是说给对象per分配了一块堆内存的空间来保存该类中的成员属性
    		/*下面要设置类的属性必须要调用方法,因为与之前不同,类的成员属性都加了封装*/
    		per.setname ("张三");
    		per.setage (18);
    		per.tell();
    	}
    }
    

    七、什么是构造方法与匿名对象?完成对象属性的初始化操作(这样即便没有栈内存的指向操作,也可以使用一次该对象,同样的由于没有栈内存的指向操作,所以该对象使用一次后就将成为垃圾空间)而有了构造方法后就可以在堆内存开辟的同时进行对象实例化处理

    class Person{
    	private String name;
    	private int age;
    	/*Person的构造方法,可以对成员属性进行初始化,但如果还需要对成员属性进行修改或获取则还需要定义 setter 和 getter 方法 ,这里偷个懒省略了,上面有类似,一般都是要加上的*/
    	public Person(String tempname,int  tempage){
    		name = tempname;
    		age = tempage;
    	}
    	/*输出方法*/
    	public void tell(){ 
    		System.out.println("姓名: " + name + "年龄: " + age); 
    	}
    }
    public class JavaDemo{
    	public static void main(String args[]){
    		Person per = new Person("张三",18);	//声明并实例化对象(但其实也可以不声明直接实例化对象,这就是匿名对象了)
    		/*如:new Person("张三",18).tell 这样代码就简洁了许多,直接一步到位了,但是这种没有指向对象的实例化操作,不好对该类进行后续的操作*/
    		per.tell();
    	}
    }
    			
    

    八、什么是this关键字?:它表示当前对象的属性和方法

    事实上不加this也可以调用类中的属性和方法,但是类中成员属性和方法参数由于表示含义的需要,有可能会产生重名定义的问题。类中的this会随着执行对象的不同而表示不同的实例
    注意:如果一个类中存在了多个构造方法的,并且这些构造方法都使用了this()相互调用,那么至少保留一个构造方法没有调用其他构造,以作为程序的出口

    九、简单的Java类需要具备哪些条件?

    1.类的名称一定要有意义,可以明确的描述某一类事物
    2.类中的所有属性都必须使用pravite进行封装,封装后的属性必须提供setter(),getter()方法
    3.类中可以提供有无数多个的构造方法,但是必须要保留无参构造方法
    4.类中不允许出现任何的输出语句,所有内容的获取必须返回
    5.【可选】可以提供一个获取对象的详细的信息的方法,可以将此方法设置为getInfo()

    /*定义一个描述部门的简单的Java类*/
    class Dept {					//满足第一点类的名称是“部门”的英文,可以明确描述某一类事物
    	private long deptno;
    	private String dname;
    	private String loc;
    	/*以上满足第二点类中的所有属性都使用了pravite进行了封装*/
    	public Dept(){};				//满足了第三点提供了无参构造方法
    	public Dept(long deptno,String dname,String loc){	//通过this来调用成员属性,就不用再在构造方法的参数上想新的名字了,这样代码会更简洁易读
    		this.deptno = deptno;
    		this.dname = dname;
    		this.loc = loc;
    	}
    	public String getIofo(){
    		return "【部门信息】部门编号:" + this.deptno + "、部门名称:" + this.dname + "、部门位置:" + this.loc;		//满足第四个条件类中所有内容的获取都用的是返回 
    	}
    	//setter(),getter()方法省略
    }
    
    public class JavaDemo{
    	public static void main(String argc[]){
    		Dept dept = new Dept(10,"技术部","北京");	//实例化例的对象
    		System.out.println(dept.getIofo());		// 获取对象的信息
    	}
    }
    
    

    程序执行结果:
    在这里插入图片描述

    十、什么是static关键字?:用于全局属性和全局方法的声明

    1.如果想要类中的属性定义为公共属性(所有对象都可以使用的属性),则可以在声明属性前加上static光键字,而当有一个对象修改了static属性内容后,将会影响到所有的对象
    2.static的定义方法?:可以在没有实例化对象的情况下直接调用static定义的属性和方法。static结构可以不受到,对象实例化的限制,并且可以实现多个实例化对象的共享操作
    3.应用案例:对在通过调用进行对象实例化的过程中,可以通过static关键字对对象的个数进行累加处理

    class Chinese{
    	private String name;
    	private int age;
    	static String country = "中华人民共和国";
    	public Chinese(String name,int age){
    		this.name = name;
    		this.age = age;
    	}
    	//setter(),getter() 省略
    	public String getInfo(){
    		return "姓名:" + this.name + "、年龄:" + this.age + "、国家:" + this.country ;
    	}
    }
    
    public class JavaDemo{
    	public static void main(String argc[]){
    	Chinese perA = new Chinese("张三",18);
    	Chinese perB = new Chinese("李四",19);
    	Chinese perC = new Chinese("王五",20);
    	perA.country = "伟大的中国";//因为country为static属性,当对象perA修改country后,对象preB和preC的country也会跟着被修改
    	System.out.println(perA.getInfo());
    	System.out.println(perB.getInfo());
    	System.out.println(perC.getInfo());
    	}
    }
    /*本程序定义了一个描述中国人的类Chinese,类中country为static(公共属性),即该属性会保存在全局数据当中,当有一个对象修改了static属性内容后将会影响到其他的所有对象*/
    

    程序执行后的结果:
    在这里插入图片描述

    十一、什么是代码块?:使用 { } 定义起来的一段程序

    1.什么是普通代码块?:将一个方法中的代码进行分割
    2.什么是构造代码块?:将一个代码块定义在一个类中(每一次实例化新的对象都会调用构造块)
    3.什么是静态代码块?:如果一个构造代码块上使用了Static关键字进行定义的话,那么该代码块就表示静态代码块(静态代码块会优先与构造代码块执行,并且静态代码块中的代码只会执行一次)

    十二、小结

    1.面向对象的程序设计是现在主流的程序设计方法,他有三大主要特性:分装性、继承性、多态性
    2.类与对象的关系:类是对象的模板,对象是类的实例(特别注意类只能通过对象才能使用
    3.**类的组成?😗*成员属性、方法
    4.对象的实例化格式:类名称 + 对象名称 = new + 类名称() (new用于堆内存空间的开辟)
    5.如果一个对象没有被实例化而直接调用,则使用时会空指向异常(如果是用static关键字定义的,可以被调用一次)
    6.类属于引用数据类型,进行引用传递时,传递的是堆内存的使用权(这句话很重要,之前讲引用传递的时候,绕来绕去,都是这些东西,即一块堆内存可以被多个栈内存所指向,而一块栈内存只能保存一块堆内存的地址)
    7.类的封装性:通过pravite关键字进行修饰,被封装的属性不能被外部直接调用,而只能通过setter或getter()f方法完成。只要是属性,类中属性必须全部封装
    8.构造方法可以为类中的属性进行初始化,构造方法与类的名称相同,无返回值,无类型声明,有封装。如果在类中没有明确定义出构造方法,则自动生成一个无参的什么都不做得构造方法。构造方法可以重载但必须至少有一个
    9.在Java中使用this关键字可以表示当前的对象,可以调用本类中的属性,但调用时要求要放在构造方法的首行
    10.使用static声明的属性和方法可以直接由类名称调用,static属性是所有对象共享的,所有对象都可以对其进行操作

    小小温馨提示:码字不易,希望能一键三连哦🤞🤞🤞

    展开全文
  • NOTEBOOK支持向量机(分类问题公式python实现)此notebook包括:1、支持向量机介绍2、什么是线性可分类支持向量机3、什么是线性分类支持向量机4、硬间隔化和软间隔化5、什么是线性分类支持向量机的对偶形式6、非线性...
  • 和对象练习题

    千次阅读 2021-02-28 16:26:50
    D 、既不是程序员定义的方法也不是现有的方法37、利用方法中的() 语句可为调用方法返回一个值( A )A、return B 、back C、end D 、以上答案都不对 38、( A)方法用做返回两个参数中的较大值( ) A 、max B 、maximum ...
  • Java抽象

    2021-03-18 00:47:04
    抽象类其实就是可以理解为是一种父类,但是这种类不能直接实例化,必须要被继承,中一样可以添加成员变量,成员方法(包括抽象和非抽象方法):public abstract class Human{private String name;private int age;...
  • (事务可以是具体的物体或行为)以圆为,圆是具有圆周率(pi)和半径(r)两个相似特征的属性。根据相似特征抽象出圆,每个圆的半径可以不同,那么半径可以作为圆的实例属性;而每个圆的圆周率pi是相同的,那么圆周率...
  • 1. 的定义和使用2. 对象的三大特性2.1 封装(1)通过对象调用被封装的内容(2)通过self间接访问被封装的内容2.2 继承2.3 多态3. 属性(变量)绑定3.1 属性绑定3.2 实例属性绑定4. 属性引用4.1 属性引用4.2 实例...
  • 文章目录一、python的与对象1.1 基本概念1.2 主要知识点1.2.1 属性和实例属性区别1.2.2 self 是什么?1.2.3 Python 的魔法方法__init__1.2.4 继承和覆盖(super().\__init\__())1.2.5公有和私有1.3魔法方法...
  • Python中的方法、实例方法、静态方法、构造方法python基础知识回顾(Class): 用来描述具有相同的属性和方法的对象的集合。...数据成员:变量或者实例变量用于处理类及其实例对象的相关的数据。方法重写:...
  • 为了搞清楚加载,竟然手撸JVM!

    千次阅读 热门讨论 2020-12-31 09:58:06
    作者:小傅哥 ... 沉淀、分享、成长,让自己和他人都能有所收获!???? ...当学习一个新知识不知道从哪下手的时候,最有效的办法是梳理这个知识结构的脉络信息,汇总出一整张的思维导出。接下来就是按照思维导图...例如:
  • Python的实例化对已定义好的进行实例化,其语法格式如下:类名(参数)定义时,如果没有手动添加 __init__() 构造方法,又或者添加的 __init__() 中仅有一个 self 参数,则创建对象时的参数可以省略不写。...
  • 第7.14节 Python中的实例方法详析

    千次阅读 2020-12-19 01:11:31
    第7.14节 Python中的实例方法详析一、 实例方法的...定义实例方法与定义函数基本相同,只是Python要求实例方法的第一个形参必须为self,也就是实例对象本身,因此实例方法至少应该有一个self参数。关于self的说...
  • Java动态获取实现某个接口下所有的实现对象集合最近有个需求,我需要获取所有同一类型的定时任务的对象,并自动执行。我想的方案是:直接获取某个接口下面所有的实现的对象集合,方便以后只需要 实现这个接口,...
  • 2016-11-17 13:40黄瞩信 客户经理比如,只定义了一个抽象方法run(),... 另一种途径是通过一个去继承接口runnable来实现线程的创建,一个线程是否处于运行状,将帐本看成数据段:、分配必要的资源后才能进入可运行状态...
  • 面试必问的 JVM 加载机制,你懂了吗?

    千次阅读 多人点赞 2021-08-14 14:49:36
    JVM 加载机制高频面试题
  • softmax激活函数用来分类,和主分类器一样预测1000个,但在推理时移除。 辅助分类器的作用的是一方面增加了反向传播的梯度信号,帮助低层特征训练,从而低层特征也有很好的区分能力,另一方面辅助分类器提供了...
  • 面试题剖析 1. 今日面试题 今天 壹哥 带你复习一个不是很容易理解的知识点,即泛型。关于泛型的面试题其实不是很多,常见的题目如下: 请说一下泛型的作用,泛型和泛型方法有什么区别? 2. 题目剖析 ...
  • JAVA Future详解

    万次阅读 多人点赞 2021-04-11 23:16:51
    以上面的代码为,CompletableFuture之所有会有那么神奇的功能,完全得益于AsyncSupply(由上述代码中的supplyAsync()方法创建)。 AsyncSupply在执行时,如下所示: public void run() { CompletableFuture<T> ...
  • 【C++与对象】第一篇:的介绍this指针

    多人点赞 热门讨论 2021-05-25 17:45:38
    的访问限定符封装6.的对象大小的计算7.的成员函数的this指针 面向过程和面向对象的初步认识 我们先初步认识一下面向过程和面向对象: 我们学c语言,c语言就是面向过程,关注的是过程,分析求解问题的步骤,...
  • 我们以二分类为,就是将一个固定维度的句子或文档向量变为一个二维向量,然后将该二维向量通过一个非线性函数映射成概率分布。 举个例子,假设最终的句子向量是一个 8 维的向量,w 是权重参数,计算过程如下: ...
  • Python学习笔记(十二):和对象

    万次阅读 2021-02-12 21:20:21
    Python学习笔记(十二):和对象 关于和对象Python学习笔记(十二):和对象一.父类方法重写变量二.调用父类的构造方法super()三. __slots__四.动态创建type() 一.父类方法重写 体中、所有函数之外:此...
  • java中string是什么

    2021-03-18 08:41:51
    java中String的使用频率非常高,那让我们来看一下它到底是什么?String是不可变对象java.lang.String使用了final修饰,不能被继承。Java程序中的所有字面值,即双引号括起的字符串,如"abc",都是作为String的...
  • JVM参数详解Arthas使用

    千次阅读 2021-12-14 17:14:33
    介绍JVM垃圾回收制度、垃圾回收算法、垃圾收集器、JVM参数、arthas使用相关问题实战。
  • Java—抽象和接口

    多人点赞 热门讨论 2021-09-06 10:31:17
    抽象与接口抽象定义和语法理解抽象作用抽象总结:接口概念接口特性注意事项:实现多个接口抽象和接口的区别? 抽象 定义和语法 包含抽象方法的,叫做抽象 需要用abstract修饰这个 在Java中,一...
  • [C/C++]详解C++的和对象

    万次阅读 多人点赞 2021-05-24 13:09:01
    是现实世界或思维世界中的实体在计算机中的反映,它将数据以及这些数据上的操作封装在一起。对象是具有类型的变量。和对象是面向对象编程技术中的最基本的概念。 1.面向对象 首先来理解什么是面向对象编程。 ...
  • 模型与 Class 实例的位置4. 数组的加载3. 过程二:Linking( 链接) 阶段环节 1:链接阶段之 Verification ( 验证)环节 2:链接阶段之 Preparation ( 准备)环节 3:链接阶段之 Resolution ( 解析)4. 过程三:...
  • jvm中篇-06-的加载过程详解1. 概述过程一:Loading(加载)阶段1. 加载完成的操作2. 二进制流的获取方式3. 模型与Class实例的位置4. 数组的加载过程二:Linking(链接)阶段1. Verification(验证)2. ...
  • (也就是将h个patch变成了一个h维的特征向量,再进行全连接分类) 维度变化过程: 现在以一个3x224x224图像处理为,设置p为7也就是设置patch_size大小为7,所以会得到32x32个patch,每个patch多携带的信息维度...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 407,756
精华内容 163,102
关键字:

参数类及其实例类