第五章 聊一聊Java的几个常用关键字

聊一聊Java的几个常用关键字

Java 8中的关键字如下图所示,其中const和goto保留。true,false,null,var看起来像关键字,但是其实是字面量(字面量是基本类型值的源代码体现,如true,false是boolean类型的字面量,var则是JDK10中局部变量推断中的字面量),字面量也不能在代码中用它们作为标识符。

在这里插入图片描述

下面分别谈论下Java面试中常问的关键字。

类相关的关键字

class

class是对某种类型对象定义变量和方法的原型。每个class在编译时会生成.class文件。在运行阶段,JVM根据需要,通过classLoad加载到方法区中。获取Class对象通常有三种方式,且在同一个类加载器中多次获取class对象返回的堆地址引用相同。

  1. Class.forName(“全限定名类名”)。 全限定类名,就是类名全称,包路径用".“隔开,在编译时,会将”.“替换为目录分隔符”/"。这种方式会在方法区查找是否被当前SystemLoader加载过,如果未加载,则加载;否则根据方法区中的类信息,在堆中根据需要创建,并将引用指向堆中该对象首地址。如jdbc的加载,Class.forName(“com.mysql.jdbc.Driver”);

  2. 实例对象.getClass()。

  3. 类名.class。 采用这种方式创建Class对象的引用时,并不会将该class对象从磁盘加载到JVM方法区,而是延迟到使用时加载。

extends

当前类需要继承某个类的相关属性及方法时,可以使用extends,但当前类不能继承声明为final的父类(如不能继承String和StringBuffer类),但可以继承声明为abstract的父类。显然父类的私有成员属性和私有成员方法不能被继承。可以通过非私有方法(如proteced,public)访问父类的私有成员属性。Java中只支持单继承,即一个类只能继承一个父类。在编译时,会先编译父类,再编译当前类,如果当前类中的方法与父类中的方法签名相同,则会覆盖父类中的方法,如我们经常会覆写Object类的toString()方法。如果属性与父类相同,在Dubbo中采用Hession序列化时,就会出现父类的值在获取为null。在运行时,可能也会先实例化父类。为啥是"可能",涉及到被动实例化与主动实例化。在后面章节中有详细讲解。关于继承的初始化先后顺序将在JMM中讲解。

implements

implements用于某个类实现一个接口类的相关方法。实现一个接口,不必实现接口的所有方法。如当前类是抽象类时,则可以不在当前类中实现接口类的所有所有方法。一个类可以实现多个接口,一个接口也可以有多种实现方式。其中一个很重要的接口是Serializable接口,如果某类实现了该接口,可以将该类的对象及值持久化到磁盘,可以允许该对象的生命周期长于该JVM的生命周期。

关于Serializable接口

  1. 序列化的是对象,不是类。与类相关的静态变量是不能被序列化的。读者测试时,需要分两个步骤,第一步是先序列化对象到本地,第二步是重新运行程序读取对象。这才算是正常的测试。这是因为类的静态属性是在编译时放入了类的常量属性中,运行时虚拟机在类加载过程中的初始化阶段会将静态变量初始化为我们的值,如果不重启程序,将会读取该值。也就是说,在RPC中(如Dubbo中)的属性定义为static将会导致该值获取不到。如下示例中,ObjectInputStream与ObjectOutputStream对象可以序列和反序列化对象,而writeObject和readObject方法可以灵活定义序列化策略。

  2. 需要注意的是,示例中有个transient修饰的phone变量,这个关键字是可以阻止该变量序列化。对于transient变量的序列化的值为JVM的初始值,即对象为null,基本类型变量为0。transient关键字可以防止某些敏感数据发送到外部系统,在一定程度上保证了序列化对象的数据安全。当使用JSON.toJSONString()时同样不能进行反序列化。

public static void main(String[] args) throws Exception {
       // serializeTestObject();先打开注释序列化到本地,然后注释后重新启动会发现static属性并未获取序列化的值
        TestSerializable test = deserializeTestObject();
        System.out.println(test.toString());
        System.out.println(JSON.toJSONString(test));//JSON处理时,同样不能进行反序列化{"name":"testName"}
    }

    private static TestSerializable deserializeTestObject() throws Exception {
        //ObjectInputStream代表对象输入流:
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\test\\testSerialize.txt"));
        //它的readObject()方法从一个源输入流中读取字节序列,
        // 再把它们反序列化为一个对象,并将其返回
        TestSerializable test = (TestSerializable) ois.readObject();
        ois.close();
        System.out.println("对对象进行反序列化操作 成功");
        return test;
    }

    private static void serializeTestObject() throws IOException {
        TestSerializable test = new TestSerializable();
        test.setName("testName");
        test.setIdNo("432550000013413243");
        test.setPhone("15102802575");
        // ObjectOutputStream 对象输出流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\test\\testSerialize.txt"));
        //它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,
        // 把得到的字节序列写到一个目标输出流中。
        oos.writeObject(test);
        System.out.println("对对象进行序列化操作---成功!!!");
        oos.close();
    }


}
//省略get,set
class TestSerializable implements Serializable {
    private String name;
    private static String idNo;
    private transient String phone;
    @Override
    public String toString() {
        return "TestSerializable{" +
                "name='" + name + '\'' +
                ", idNo='" + idNo + '\'' +
                ", phone='" + phone + '\'' +
                '}';
    }
}

方法相关的关键字

case

case经常和switch搭配使用。swith变量类型能支持int、short、char、byte、enum和String(JDK1.7后支持),case后面只能是常量、可以是运算表达式,但类型一定符合前面要求,且不能是变量。当运行case时,若未找到匹配的case,则执行default。当case后没有break时,则顺序执行每个case,直到遇到break。注意default语句同样也需要break。

关于switch case效率要高于if else

在C语言或JAVA语言中,switch case就比if else的效率要高。为啥呢?当时我们通过反编译后发现,if else 反编译后为汇编指令为cmp jmp->cmp jmp…,java中反编译的字节码是if_icmpne指令。也就是说,if esle 是一直执行比较,跳转。直到比较相等,则跳转到对应的代码执行。但switch case则是在编译时,将switch case转化为跳转表,并将跳转表放在只读数据区。当变量与表格中的常量进行比较后,直接跳转到对应表格值所指的地址执行。所以,这是以提供空间复杂度为代价降低时间复杂度的手段。

public class TestCase {
    public static void main(String [] args) {
        int i = 5;
        switch (i) {
            default:
                System.out.println("5");//这里没有break,将导致5和1都打印
            case 1:
                System.out.println("1");
                break;
            case 2:
                System.out.println("2");
                break;
            case 3:
                System.out.println("3");
                break;
            case 4:
                System.out.println("4");
                break;
        }
        System.out.println("-------------------");
        testIf(5);

    }

    private static void testIf(int i) {
        if(i == 1) {
            System.out.println("1");
        } else if( i == 2) {
            System.out.println("2");
        } else if( i == 3) {
            System.out.println("3");
        } else if( i == 4) {
            System.out.println("4");
        } else if( i == 5) {
            System.out.println("5");
        }
    }
}
//输出结果,从main方法执行结果可以看出default也需要break语句。
5
1
-------------------
5
//javap -c TestCase > TestCase.txt反编译后的跳转表
 public static void main(java.lang.String[]);
    Code:
       0: iconst_5
       1: istore_1
       2: iload_1
       3: tableswitch   { // 1 to 4
                     1: 40
                     2: 51
                     3: 62
                     4: 73
               default: 32
          }
       ...

native

native关键字是Java调用操作系统资源的入口,是Java跨语言跨平台的基石。在JDK中很多地方有用到,比如,多线程、文件操作,Ojbect对象中的hashCode,getClass()等方法都是native方法。因为这些功能都与操作系统有关。需要通过JNI(Java Native Interface)调用操作系统相关函数,或者调用另外语言编写的类库。所以,从Java1.0开始就已经集成了JNI标准。因为native关键字是JDK帮我们封装了底层不同操作系统下的接口操作,而我们自己用native将导致java不再支持跨平台操作。如作者之前在android系统下开发了一个与APP加密相关的功能,同时通过JNI方式与连接在该android系统上的RFID读写器进行通信。当时该功能采用JNI的方式进行调用的。需要将C语言编译成linux下的动态链接库.o文件。而在windows下平台,该动态链接库就不能再使用了。

因为JNI是Java调用操作系统资源的唯一方式,如果能对JNI进行了解,对Java底层调用就会更加清楚。所以,这里进行必要的讲解,没兴趣的读者可以略过。以下实例来源于官网,对JNI感兴趣的读者可以访问参考[官方文档]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html

JNI编程步骤
  1. 在java文件中根据需求编写native方法,包括入参是什么,返回什么类型等。native可以与除了abstract的其他所有的标识符连用。通过Object类可以得知,所有我们定义的类都继承Object。我们也能调用Object的hashCode等本地方法,也可以重写hashCode等本地方法。所以,读者完全可以把本地方法当做普通的java方法对待。
   package pkg;
   
   public class Cls {
       native double f(int i, String s);//声明native方法告诉jvm调用的是本地方法。
       static {
           System.loadLibrary("pkg_Cls");//加载名为"pkg_Cls"的动态链接库。
       }
   }
  1. 使用JDK自带工具javac编译该类,生成.class文件。
   javac Cls.java
  1. 使用javah -jni Java类名生成C语言中的.h头文件。
   javah -jni pkg.Cls

生成后的文件pkg_Cls.h内容如下:

   /* DO NOT EDIT THIS FILE - it is machine generated */
   #include <jni.h>//jni.h 这个文件,在/%JAVA_HOME%include
   /* Header for class pkg_Cls */
   
   #ifndef _Included_pkg_Cls
   #define _Included_pkg_Cls
   #ifdef __cplusplus
   extern "C" {
   #endif
   /*
    * Class:     pkg_Cls
    * Method:    f
    * Signature: (ILjava/lang/String;)D
    */
   JNIEXPORT jdouble JNICALL Java_pkg_Cls_f
     (JNIEnv *, jobject, jint, jstring);
   
   #ifdef __cplusplus
   }
   #endif
   #endif
  1. 创建c或cpp文件,使用C或C++实现该头文件中的方法。
   #include "pkg_Cls.h"
   #include <stdio.h>
    
   JNIEXPORT jdouble JNICALL Java_pkg_Cls_f
     (JNIEnv *, jobject, jint, jstring)
   {
       printf( "int=%d,string=%s", jint,jstrings);    
   }
  1. 将该方法编译成所需平台的动态链接库。windows平台是dll文件。而linux平台下是.o文件。注意该动态链接库的位置。

在windows平台上,通过MinGW GCC运行如下命令可以获取dll文件,最后将该dll文件放入到java中的Cls.java同级目录下,运行就可以通过java语言调用c语言代码在控制台打印出对应方法的值。在该Cls类加载时统一加载到Java程序的地址空间。

gcc -m64  -Wl,--add-stdcall-alias -I"C:\Program Files\Java\jdk1.7.0_71\include" -I"C:\Program Files\Java\jdk1.7.0_71\include\include\win32" -shared -o TestNative.dll TestNative.c

final与finally

final关键字

final关键字可以用于类、成员变量、局部变量和方法前面。

  1. final修饰类

final修饰类表示该类不能被继承,Java SDK中最常见的final类有String和Math。当final修饰类是,其中所有的成员变量及方法都隐含的定义为final。

  1. final修饰成员变量/方法参数变量

final修饰成员变量时,代表该变量是一个常量,在编译时,将替换为编译后常量池中指定的地址,如String字符串。显然,如果final修饰基本变量类型时,该变量一旦初始化,将不能被修改。如果修饰为引用类型,引用所指的对象在堆中的首地址将不能人为修改,只能被垃圾收集器修改。但是可以改变引用中属性的引用所指向的地址值,这点是需要特别注意的。

关于final修饰成员变量的初始化,有两种方式,第一种方式是直接在声明时赋值,第二种方式是在构造函数中进行初始化,否则编译校验会报错。因为final修饰的常量只能赋值一次,而任何对象实例化时,都需要先调用构造方法,所以,保证了类中的常量在使用时被初始化为"常量"。

private final int a;
public FinalTest(int a) {
    this.a = a;
}

final修饰方法参数时表示该参数为只读,基本类型则不允许重新赋值。引用类型不能修改引用所指向的地址。

  1. final修饰成员方法

final修饰成员方法可以防止继承类对它的修改(重写)。如果父类中final方法的访问权限为private,子类将不能调用父类的private方法,可以在子类中重新定义方法名相同的函数。

finally关键字

finally经常用在异常处理中。当try{}有执行时,finally才会执行。当执行try{}或catch{}时,线程被中断或者系统停电这种极端异常情况才不会执行finally。

如果finally中抛出异常,将压制catch()中的异常不能抛出来,所以,我们将得不到准确的异常堆栈信息。建议finally中不需要再显性抛出异常,这点在阿里巴巴Java编码规范中有提到。

finally中的return语句同样也会压制catch中的return语句。如下代码,当注释(1)和(2)处,返回的是2,即返回try{}块中的return,这点毫无疑问;当打开(1)注释,返回的是却是4。这就导致了finally中的返回值压制了try{}中的返回值。同理,当打开(2)时,你可能猜测到返回多少了,不妨试下。

private static int testFinally() {
        int a = 1;
        try {
            a = 2;
            return a;
        } finally {
            a = 20;//并不会影响a的值
            //return 4;//(1)
            //a ++;//(2)
            //return a;(2)
        }
 }
final、finally、 finalize的区别

finalize不是关键字,不是关键字,不是关键字。finalize属于Object的对象中的一个方法,当垃圾收集器决定没有引用指向该对象时被调用。回收该对象时调用。一个对象的finalize方法只会被执行一次,执行之后并不意味着对象已经回收,如果自行调用finalize方法,将导致本来决定被垃圾收集器调用finalize方法而回收的对象,因人工自行调用而不能执行该方法。导致垃圾没被收集释放。

类和方法都相关的关键字

new

当程序运行到Object obj = new Object()的字节码时,JVM首先在方法区检查该new指令的参数是否能在方法区中定位到一个类对象的符号引用,检查该类是否已经加载、验证、准备、解析和初始化等步骤,当类初始化后,就已经完全确定出创建对象实例时所需的内存空间大小,然后在堆或者栈(如果没有逃逸)分配结构化内存空间,该对象的类型数据信息(如,对象类型,父类类型,实现接口类型等)都存放在方法区中。这个地方涉及到垃圾回收机制,需要考虑内存空间中产生的碎片问题,目前有两种内存分配方式,第一种是指针碰撞(Bump the Pointer),第二种就是空闲列表(Free List)执行内存分配。

Java对象的内存布局,包括对象头、实例数据和对齐填充(仅仅起占位符作用,可能不存在)。"Object obj"反映到Java栈的本地变量表中,作为一个reference引用类型数据出现。 参考《Java虚拟机规范(第7版)》的描述,JVM包含三种引用类型,分别是类型 (class type),数组类型(array type)和接口类型(interface type),这些引用类型的值则分别由类实例、数组实例以及实现了某个接口的派生类实例负责动态创建,那么JVM中究竟是如何为这些类型创建对应的对象实例呢?-------------如果是在Java语法层面上创建一个对象,无非就是使用一个简单的new关键字即可,但是在JVM中就没有那么简单了,其实牵扯到细节的实现相当复杂,而且过程繁多。简单地说,当Java语法层面使用new关键字创建一个Java对象时,JVM首先会检查这个new指令的参数能否在常量池中定位到一个类的符号引用,然后检查与这个符号引用相对应的类是否已经成功经历加载、解析和初始化等步骤,当类完成装载步骤之后,就已经完全确定出创建对象实例时所需的内存空间大小,接下来JVM将会对其进行内存分配,以存储所生成的对象实例。如下图所示:

在这里插入图片描述

Java内存是基于分代的思想进行管理,Java堆进一步细分为:新生代 ( Young )和老年代 ( Old );这也就是JVM采用的“分代思想”,简单说,就是针对不同特征的java对象采用不同的策略实施存放和回收,所用分配机制和回收算法就不一样。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。

分代收集算法:采用不同算法处理[存放和回收]Java瞬时对象和长久对象。大部分Java对象都是瞬时对象,朝生夕灭,存活很短暂,通常存放在Young新生代,采用复制算法对新生代进行垃圾回收。老年代对象的生命周期一般都比较长,极端情况下会和JVM生命周期保持一致;通常采用标记-压缩算法对老年代进行垃圾回收。

这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。那么Java堆区被细分成这么多区域,对象实例究竟是存储在堆区中的那一个区域下呢?在JVM运行数据区中,堆区和方法区是线程共享的数据区,任何线程都可以访问到这两个区域中的共享数据,由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆中划分内存空间是非线程安全的,所以务必需要保证数据操作的原子性。基于线程安全的考虑,如果一个类在分配内存之前成功完成的类加载,JVM会优先选择在TLAB(Thread Local Allocation Buffer,本地线程分配缓存区)中为对象实例分配内存空间,TLAB在Java堆中是一块线程私有数据区,它包含在Eden空间内,除了可以避免一系列的非线程安全问题外,同时还能提高内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。

当为对象成功分配好所需的内存空间(实例化)后,JVM接下来要做的任务就是-------初始化对象实例。JVM首先会对分配好的内存空间进行零值初始化,这一步操作确保了对象的实例字段在Java代码中可以不用赋初值就能够直接使用,程序能够访问到这些字段的数据类型所对应的零值。

对分配后的内存空间进行零值初始化后,JVM就会初始化对象头和实例数据。最后将对象引入栈后,再更新PC寄存器中的字节码指令地址。经过这一系列的操作步骤之后每一个Java对象实例才算是真正的创建成功。

在并发环境下从堆中划分内存空间是非线程安全的,new运算符具有-------数据操作的原子性;也就是说创建一个Java对象分配内存,要么所有步骤都成功,返回对象的引用,要么回归到创建之前的内存状态,返回为NULL。

如果我们需要比较两个new出来对象的内容的话,需要通过equal()函数去实现,这样才是比较地址里面内容的正确方式。

static

上面谈到了new关键字,新建对象是在堆上(少数在栈上)。但是如果方法是static类型,那么该方法就会在在类加载时存储到方法区。虽然在方法区,并不能说由于线程共享而访问权限扩大了。static与访问权限没有任何关系,线程共享也与访问权限没有关系。但由于线程共享,所以必须考虑方法区的数据线程安全。所以要对static修饰的方法进行加锁synchronized,以保证多线程访问是安全的。

static关键字的用法
  1. 修饰成员变量
    修饰成员变量时,表示该变量为静态变量,静态变量被类的所有对象共享,在运行时,它只在类初次加载时被初始化到方法区,并赋予默认值。加入final和不加final的static变量初始化时间是不一样的。而普通的非静态变量每创建一个对象,就会出现该非静态变量的副本。static成员变量的初始化顺序是严格按照代码中的顺序进行初始化的。静态成员变量建议使用类名来访问,访问时,直接从方法区中常量池拉取静态变量(ldc指令)到栈中操作。JSE规定,static是不允许用来修饰局部变量(方法内的变量,static方法内也是不能使用static局部变量的)。

  2. 修饰成员方法
    static修饰的方法为静态方法,静态方法是类级别的,不依赖于任何对象的,所以在静态方法中是不能使用this。因为该类的对象不一定存在,所以也不能访问非static变量或成员方法。之前讨论this关键字时有特意强调this是针对对象的。构造器中隐含包含了this参数,所以构造器是非静态的。Java的实例构造器只负责对对象进行初始化,不负责创建对象,这在反编译后会发现构造函数是调用的invokespecial指令,表示构造函数只实例构造器,而创建对象则有专门的new指令。静态成员方法在调用时从方法区加载到栈内执行

静态方法经常用来作为无状态的工具类,当功能内部没有访问到非静态数据(对象的特有数据),那么该功能可以定义为静态的。接口默认是static类型的,静态方法允许在没有创建对象的情况下进行调用。Spring中AOP不能切到静态方法,如果一定要增强,可以参考一文

  1. 静态代码块
    static关键字可以修饰代码块,且一个类中可以有多个static块。在类初次被加载时,会按照static块的顺序来执行每个static块,如果有父类静态块,则先执行父类的静态块。且都只执行一次。 我们会经常将只进行一次初始化的操作都放在static代码块中。如加载jdbc驱动程序,我们经常会这么写。
   static {
   		try {
   			Class.forName("oracle.jdbc.driver.OracleDriver");
   		} catch (ClassNotFoundException e) {
   			LOGGER.error(e.toString());
   		}
   }

静态语句块只能访问在静态语句块之前的变量,定义在它之后的变量可以赋值,但是不能访问。因为访问就是获取该值,但是static此时只有零值,还没有进行初始化。所以java规范禁止static语句块向前引用。这点也是在编译时会有校验的。

静态代码块能放在方法体中吗?这个是面试中常考的问题。我们可以分两个步骤回答这个问题。
(1)如果方法是普通方法,普通方法需要我们创建对象,通过该对象调用该普通方法执行。然而,静态代码块在类首次被加载时就已经自动执行完了。如果静态代码块嵌入在普通方法内,那该普通方式在执行时需要嵌入类加载时的部分运行结果。复杂且无意义。JDK在编译检查时禁止这样的语法。
(2)如果是static修饰的静态方法,因为静态方法同样也需要我们手工通过类名来调用,而不是直接在类加载的时候就运行了。JDK同样在编译检查时禁止这样的语法。
也就是说静态代码块能够自动执行,而不管是普通方法还是静态方法都是需要手工执行的。

  1. 静态内部类

内部类一般用于外部类的使用,如AbstractQueuedSynchronizer中的Node,ReentrantLock中的Sync。且每个内部类都能继承一个接口。该特性使外部类实现了类似C++的多重继承。内部类的一切都属于外部类,包括内部类的私有方法或属性。但外部类的一切不属于内部类,所以,内部类不能访问外部类的属性,方法。加入static关键字的内部类为静态内部类。

当内部类是static时,意味着:
(1)当创建内部静态类对象时,并不需要其外围类的对象;
(2)不能从静态类的对象中访问非静态的外围类对象。因为外部类的对象可能还没有被创建。
(3)当内部类为非静态内部类时,该内部类中不可以声明静态成员,只有当内部类修饰为静态类,才能在这个内部类中定义静态成员变量与成员方法。
(4)在一个外部类中定义一个静态内部类,不需要利用关键字new来创建内部类的实例。

内部类编译后,也会产生一个.class文件。文件名为外部类$内部类名字。而匿名内部类编译后产生外部类$1, 2 名 字 。 对 于 方 法 体 里 面 内 部 类 , 编 译 后 的 类 名 为 x x x 2名字。 对于方法体里面内部类,编译后的类名为xxx 2xxxxxx$数字+类名.class格式,一定要加数字,因为无法保证在另外一个方法里面不会出现同样的类名。

  1. 静态导入包
    JDK1.5后加入了静态导包功能,采用import static代替import。
    如下代码,则在运行时的内存结构大概如下图所示。
public class Person {
    String name;
    static int age;
    /* 其余代码不变... */
    /**Output
     * Name:zhangsan, Age:12
     * Name:lisi, Age:12
     *///~
}

在这里插入图片描述

static关键字的加载过程

类加载过程包括:加载,验证,准备,解析,初始化5个阶段。

static关键字在准备和初始化阶段有所动作。在准备阶段,将会为static变量在方法区分配内存,并对内存赋予对应类型变量的"零值"。如果变量为static final类型,在编译时将静态常量放入到类的常量池中,所以,这个时候就不是零值了,而是指向常量池对应的值。

static在类初始化阶段,才真正第一次执行该类的java代码。而java代码是由编译器根据代码自动收集生成的类构造器()。类构造器由编译器自动收集类中所有类变量的赋值动作&静态语句块中的语句合并产生的。与类构造函数(即实例构造器())不同,()不需显式地调用父类构造器,虚拟机会保证子类的()执行前,父类的()已执行完毕。并且在并发环境中能正确执行。

super与this

这两个关键字与类的对象相关,但是又是在方法体中使用,所以,这里单独讲解。

this为自身对象的引用。即该对象在堆中的起始地址指针。super为父类对象的引用。为该对象父类在堆中的指针。因为前面说到这两个关键字是与类的对象相关,所以两关键字都不可以在static环境中使用。即不能在static变量、static方法、static语句块中使用。主要有以下用法。

  1. 普通的直接引用,如每个方法内部隐藏一个this指针参数。public void test(int a)方法内的第一个参数为指向该对象本身,第二个参数才为int a。可以使用super.xxx来引用父类的非private成员变量或通过super.yyy()来调用父类中的非private方法。

  2. 方法内部形参与成员变量重名时使用。我们经常会在生成get()、set()方法看到this.a=a。this.a即表示形参a赋值给成员变量a。也可以使用super.a=a赋值父类中的a变量为形参a的值。

  3. 引用构造函数。当有多个构造函数时,我们经常会用到this(a)、super(a)就会调用该类或者该类父类的包含一个参数的构造函数,且构造函数参数类型相同。在有继承关系的子类构造函数中,第一个语句前都会隐含第调用super()方法。如果类及其父类中不存在构造函数,虚拟机为在运行时增加一个无参构造函数,并且在该类中隐含调用super()方法。

参考:
https://blog.csdn.net/javazejian/article/details/70768369
https://www.jianshu.com/p/2bf1cb39f7dd
https://www.cnblogs.com/canacezhang/p/9334192.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值