【JavaSE8 基础 Keyword】static && final 关键字总结 2019_8_4

static

修饰位置

内部类,方法,初始化设定式(block),变量

分别叫做:
静态嵌入类,静态方法/类方法,静态变量/类变量
静态初始化器(static initializers)/设定式(静态代码块/静态语句块/静态初始化块)

理解

static大多数只用于Field,Method,Statement,而并没有C/C++中的static class的概念,最多是一个静态嵌入类,表示是一个非内部类,不访问外部类。

而在Field,Method,Statement前面修饰的时候,一般都是意味着:共享

一个类有一个的静态成员,那么这个类的所有实例都可以访问这个静态成员,如果是publice修饰,那么外部也可以获取这个静态对象

或者可以理解为,static变量/函数是不因具体实例的不同而发生改变的事物/特征/行为

例如中国人都是黄皮肤,不因个体改变——》final static String color = yellow

注:因为A类的静态变量static1,可以被A的实例a任何时刻访问,也就是静态对象一定先于本类实例创建

存在意义及特性

①修饰内部类:静态嵌入类

class T { static class C {} } == T.C

作用:声明C不是T的内部类,只是被T单方面使用。

声明一个独立个体C类嵌入在T类的类体中(单独拿出来写成C.java也是可以的)。正如T的静态方法在方法体中没有T的当前实例一样,C也没有T的当前实例,也没有任何词法封闭的实例。

特性:

  • Compile-time Error:如果一个静态类包含一个封闭类的非静态成员的用法,则这是一个编译时错误。

没看懂的。大佬求救。什么是member interface?

A member interface is implicitly static (§9.1.1). It is permitted for the declaration of a member interface to redundantly specify the static modifier.

②修饰方法:类方法

class T { static method(){} } == T.method()

作用:不依赖于T类的实例对象,通过T类即可被调用。方法与调用者无关

特性:

  • Compile-time Error1:T类的静态方法不能使用本类的非静态成员变量,只能使用静态成员变量(但可在静态方法中new一个本类实例,然后通过实例去调用非静态成员变量)
  • Compile-time Error2:不能使用this和super两个特定引用对象。(可以new对象,例如上面new本类对象)
  • 类方法不能被继承,但也可以经过子类实例对象调用(通过extends找到父类地址,然后调用父类类方法)

官方称:类方法class method,及非静态方法non-static method、实例方法instance method

③修饰变量:类变量

作用:将类中实例变量无论多少次实例化都是重复的变量抽取出来成为类变量,类变量共享给每个实例

特性:

  • 类变量随类初始化而初始化。
  • 只要类变量所在类被加载,就会产生唯一类变量对象,不论这个类会产生多少个实例,类变量始终只有一个。需要注意,一个指的是数量上的一个,并不代表是不变对象,加上final才是唯一数量且不变的类变量。
  • 类变量与类方法都不被子类继承,但都可以通过子类实例对象调用,因为类的对象实例也保存了(本/父)类的引用,所以可以找到(本/父)static类变量,但不被推荐,会有warning。
  • 除了interface里的static变量不能使用super引用,普通class子类是可通过super对象访问到父类的静态变量

[外链图片转存失败(img-6b4jwL4l-1564885266660)(i\staticmf.png)]

官方称:类变量class variable==静态字段``static Fields`,非静态变量=实例变量

插:多态性—子类对父类的隐藏

【不仅仅是static,还有extends/implements导致的多态性也会造成父类的方法、属性被隐藏】

下面详细说明全部的隐藏情况

Ⅰ、子类实例【半隐藏】父类static类型方法、变量

父类的static类方法/static类变量 被 子类(必须完全相同)的static类方法/(类型可不同)static类变量隐藏

原因:经过子类实例调用的父类静态类方法/类变量会先经过子类,然后再经过extends关系找到父类的静态类方法/类变量。但是子类可以“拦路/截胡”,创建与父类完全相同的子类静态类方法,可不完全相同的子类静态类变量。依据就近原则,使用子类的同名静态类方法。从而隐藏了父类的同名静态类方法。

//1.1不完全相同,只同名即可的父子类静态变量,隐藏父类静态变量
//1.2完全相同,同函数签名同返回值的父子类静态方法,隐藏父类静态方法
class SuperClass {
    //1.1.1父类静态变量
    static int x = 2;
    //1.2.2父类静态方法
    public static void SuperSay(){
        System.out.println("SuperClass");
    }
}
public class SubClass extends SuperClass {
    //1.1.2子类同名静态变量
    static double x = 4.7;
    //1.2.2子类同函数签名同返回值同访问权限=完全相同的父类静态方法
    public static void SuperSay(){
        System.out.println("SubClass");
        //注意,此处禁止通过super.SuperSay()进行父类静态方法调用【静态方法禁止使用super,this】
    }
    
    void printX() {
        System.out.println("子类覆盖的静态x " + x + " 这里是父类x " + super.x);
    }
    
    public static void main(String[] args) {
        //隐藏条件:通过子类引用且是子类实例调用父类静态类变量
        new SubClass().printX();
        new SubClass().SuperSay();
    }
}
//console:
子类覆盖的静态x 4.7 这里是父类静态x 2
SubClass

[外链图片转存失败(img-tFklGCUB-1564885266666)(i\statichp.png)]

[外链图片转存失败(img-HUAtyqWS-1564885266670)(i\statichr.png)]

父类静态变量并不是永远隐藏(不被继承),可以在子类方法中使用super.静态变量引用到,及或通过父类引用

父类静态方法并不是永远隐藏,仍然有机会通过子类实例调用父类静态类方法,通过父类引用非子类引用

	//SubClass psvm
	public static void main(String[] args) {
        //不隐藏条件:通过父类引用但是子类实例调用父类静态类变量
        SuperClass superClass = new SubClass();
        System.out.println(superClass.x);
        superClass.SuperSay();
    }
//console
2
SuperClass

Ⅱ、子类实例函数【永久隐藏】父类实例函数

Ⅲ、子类实例变量【半隐藏】父类实例变量

class SuperClass {
    //父类实例变量i
    int i = 1;
    //父类实例方法
    public void saySuper(){
        System.out.println("SuperClass");
    }

}
public class SubClass extends SuperClass {
    //子类覆盖的实例变量i
    double i = 10;
    //子类复写的实例方法
    @Override
    public void saySuper(){
        System.out.println("SubClass");
    }

    public static void main(String[] args) {
        //父类引用,子类实例【这里省略了子类引用,子类实例的情况(肯定是调用子类)】
        SuperClass superClass = new SubClass();
        System.out.println(superClass.i);
        superClass.saySuper();
    }
}
//console
1
SubClass

父类实例变量不被隐藏,而父类方法仍旧是跨过了父类实例方法,直接调用了子类实例方法。亦称虚方法调用

④修饰陈述:静态初始化器

static {block}

特性:

  • Compile-time Error1:static初始化器,不能使用this,super(this,super应用依赖类实例对象,static不依赖类的实例即可使用)
  • Compile-time Error2:名称引用任何实例变量
  • 类中声明的静态初始化器,在类初始化时执行且只执行一次(整个程序生命周期)

延展:实例初始化器

作用:对本类(实例、静态)对象进行初始化

特性:

  • Compile-time Error1:不可有return语句
  • 可以使用this与super引用对象,并在范围内使用任何类型变量(static)
  • 在类的实例化时执行一次,多少个实例就执行多少次

拓展:类初始化顺序

①先父类后子类

②先静(第一次加载)后实

③静/实 变量表达式 与 静/实初始化器 谁前谁先(静/实初始化器可有多个)

④最后一定是构造函数作为最终操作

【具体的实操debug案例,会在class,interface,abstract联合博文中给出】

final

修饰位置

①类,②函数,③(变量,函数内部变量,接受器参数)

3种位置有不同的存在意义

理解

A class can be declared final if its definition is complete and no subclasses are desired or required.Both class and instance variables (static and non-static fields) may be declared final.

final可以和static一起修饰:
对于 Static Variable,意义为:静态不变量(对象是不变引用,基本类型是不变内容)
对于 Static Method 来说,意义为:不可隐藏父类的静态函数/类函数

作用及特性

①修饰类:最终类

作用:禁止继承扩展
特性:
1.final类不能有子类。
2.final类不代表整个类的方法属性都被final修饰。final类的属性依旧可以使用final修饰。
(final类的方法不需要用final修饰,因为final类就达到了final方法的作用)
3.与abstract关键字完全相反

②修饰方法:最终方法

作用:防止子类Override父类实例方法,防止子类静态方法hiding父类静态方法

  • 子类Override父类实例方法,隐藏父类实例方法
    SuperClass superClass = new SubClass();
    SubClass superClass = new SubClass();
    不论①、②,一般情况子类实例对象调用实例方法一定会隐藏父类的实例函数
    一般情况=子类复写的前提下,且实例方法内部未显式调用 super.instanceMethod()

  • 子类静态方法hide父类静态方法
    子类拥有与父类同静态函数签名同返回值,访问权限>=父类。通过子类实例/子类调用同名静态函数时,父类函数被隐藏
    (阻断执行父类的静态函数,而是执行就近的子类静态函数)

特性:
1.父类 final 实例方法不能被子类override,可以被继承
2.父类 final 静态方法不能被子类hiding

③修饰变量:不变量

作用:基本类型变量完全不可变,非基本类型变量引用指向不可变,变量属性可变。

特性:
1.不能改变非基本类型变量的引用,但仍可改变非基本类型变量属性
2.final变量允许在没有同步(synchronized)下实现线程安全的线程类成员变量
3.final静态变量及实例变量必须初始化一次(by a static initializer,at the end of every constructor)可延后在类构造器内赋值

实例

//Test类
public class ThreadFinalTest {
	//psvm
    public static void main(String[] args) throws InterruptedException {
	    MyRunable myRunable = new MyRunable();
	    {
	    	//1个MR实例的多子线程并发、并行执行
	        new Thread(myRunable).start();
	        new Thread(myRunable).start();
	        Thread.sleep(2000);
	        System.out.println(myRunable.point.toString());
	    }
        
        {
        	//2个MR实例的各自单独子线程并发、并行
	        //两个MR实例,也就意味着只有2个不同的point实例对象,隶属于2个MR实例
	        MyRunable myRunable = new MyRunable();
	        MyRunable myRunable1 = new MyRunable();
	        //这个时候两个线程执行的MR实例都不一样,当然读取到的MR.point对象也不一样
	        new Thread(myRunable).start();
	        new Thread(myRunable1).start();
	        Thread.sleep(200);
	        //那么最后主线程拿到的就是最后一个MR实例子线程的sout point值
	        System.out.println(myRunable.point.toString());
	        
	        //我们顺便来搞事,待会在MR类中final与static搭配使用
	        //重点来了,如果你对final Point point 加了一个static关键字
	        //那两线程point对象又同步了,因为这两个MR实例都共享1个point实例对象
	    }
    }
}
//MR类
class MyRunable implements Runnable {
    //final 基本数据类型成员(不变量,引用与内容都不可变,你改 1 对象的内容就是改引用)
    final int i = 1;
    //final 类实例类型成员(不变量,不变的是引用,实例内部属性可变)
    final Point point = new Point(); //这个point在这还是run()里new都是差不多的
    //搞事情——final+static不变量
    //final static Point point = new Point(); 

    @Override
    public void run() {
        System.out.println(Thread.currentThread() + " " + i + " " + point.toString() + " || " +
                new SimpleDateFormat("HH.mm.ss").format(new Date()));
        point.x = (int) (Math.random() * 100 + Math.random() * 100);
        point.y = (int) (Math.random() * 100 + Math.random() * 100);
        System.out.println(Thread.currentThread() + "" + i + " " + point.toString() + " || " +
                new SimpleDateFormat("HH.mm.ss").format(new Date()));
    }
}
//成员类
class Point {
    int x = 0;
    int y = 0;
    @Override
    public String toString() {
        return "Point{" +
                "x=" + x +
                ", y=" + y +
                '}';
    }
}

//console
//线程安全
//1个MR实例的多子线程并发执行
Thread[Thread-0,5,main]1Point{x=0, y=0} || 17.50.40
Thread[Thread-0,5,main]1Point{x=86, y=77} || 17.50.40
Thread[Thread-1,5,main]1Point{x=0, y=0} || 17.50.40
Thread[Thread-1,5,main]1Point{x=106, y=50} || 17.50.40
Point{x=106, y=50}
//1个MR实例的多子线程并行执行
Thread[Thread-1,5,main] 1 Point{x=0, y=0} || 18.07.11
Thread[Thread-0,5,main] 1 Point{x=0, y=0} || 18.07.11
Thread[Thread-0,5,main] 1 Point{x=82, y=105} || 18.07.11
Thread[Thread-1,5,main] 1 Point{x=82, y=105} || 18.07.11
Point{x=82, y=105}
//2个MR实例的单独子线程并行、并发执行与上面结果相同。
//就不贴出来了,自己简单添加static再 Shift F10 就知道了。

final 性能优化

JVM 还保存final static variable的优化,已不存在final class / method的内联性能优化的说法

阿里Java final 规约

【推荐】final可提高程序响应效率,声明成final的情况:

  1. 不需要重新赋值的变量,包括类属性、局部变量;

  2. 对象参数前加final,表示不允许修改引用的指向;

  3. 类方法确定不允许被重写。

JVM发展到现在,我们声明 final 的目的已经不再主要是性能问题了,而是正确性、合理性、严谨性的问题。用来提醒自己以及其他人,这里的参数/变量是真的不能被修改,并让Java编译器去检查到底有没有被乱改 ——陈先生
下面是陈先生原文

所有JVM都相关:
	方法参数与局部变量用final修饰是纯编译时信息,到Class文件里就已经没有踪迹了
	JVM根本不会知道方法参数或者局部变量有没有被final修饰。这个是完全不可能影响性能的。

HotSpot JVM相关:
	1.static final variable是好的,HotSpot VM里的JIT编译器会利用这个信息来做优化;
	2.实例变量(字段)用final修饰
		目前(直到最新的JDK8u)的 HotSpot VM 都不会使用这个信息来优化。
		只有JDK自身的一些核心类被区别对待(例如说java.lang.String上的final成员)
		
		所以 final non-static variable不会影响性能。
	3.实例方法用final修饰
		3.1在可以使用final修饰的场景下
			就算没有用final修饰,HotSpot VM的JIT编译器也一样会通过
			类层次分析(Class Hierarchy Analysis,CHA)
			来发现这个方法是实质上只有一个实现版本的。
			
			所以有无final method 生成的代码质量会一样,性能不会有区别。
			
		3.2在不能使用final修饰的场景下
			如果当前加载的类中只有单一实现(别的实现 在 尚未加载或已经卸载的类中)
			则 HotSpot VM 仍然可以通过CHA把它当作单一实现来做激进优化,
			并通过deoptimization来保证安全。

	
	总结:在HotSpot VM的环境中,实例方法没有必要为了性能而加final,
	而是真的希望表达语义上的限制时才应该使用。

官方 SE8 文档

The Java® Language Specification Java SE 8 Edition 8.4.3.3 final method
final类及final方法优化特性

final class Point {
    int x, y;
    void move(int dx, int dy) { 
        x += dx;
        y += dy; 
    }
}

class Test {
    public static void main(String[] args) {
        Point[] p = new Point[100];
        
        for (int i = 0; i < p.length; i++) {
            p[i] = new Point();
            
            p[i].move(i, p.length-1-i);
        }
        /**inline
        for (int i = 0; i < p.length; i++) {
            p[i] = new Point();
            
            Point pi = p[i];
            int j = p.length-1-i;
            pi.x += i;
            pi.y += j;
    	}
    	**/
    }
}
//在运行时,机器代码生成器或优化器可以“内联” final方法的主体,用主体中代码替换方法调用,可反编译查看
//内联过程必须保留方法调用的语义【完整的方法执行过程】。
//特别是,如果实例方法调用的目标为null,则即使方法是内联的,也必须抛出NullPointerException。
//Java编译器必须确保在正确的位置抛出异常,以便在方法调用之前以正确的顺序评估方法的实际参数。
//这种内联不能在编译时完成,除非可以保证Test和Point将始终一起重新编译,这样每当Point(特别是它的move方法)发生变化时,Test.main的代码也将被更新。

但实际上,我自己测试final类我只见过少数次(1-2次)内联情况
我甚至拆出2个.java文件,下面给出我的测试代码

//测试类
public class FinalTest {
    public FinalTest() {
    }

    public static void main(String[] args) {
        FinalT2 finalT2 = new FinalT2();
        finalT2.xxx();
        finalT2.say();
    }
}
//final类
public final class FinalT2 {
    void xxx(){
            System.out.println("xxxx");
    }

    final void say() {
        for (int i = 0 ;i < 3;i++)
            System.out.println("hhhh");
    }
}
//FinalTest.class 原样没变
public class FinalTest {
    public FinalTest() {
    }

    public static void main(String[] args) {
        FinalT2 finalT2 = new FinalT2();
        finalT2.xxx();
        finalT2.say();
    }
}

参考文献

iroan:Java子类与父类方法的隐藏和覆盖
陈先生:深入理解final关键字以及一些建议
Mrzhoug:面试题–JAVA中静态块、静态变量加载顺序详解
格色情调1984:静态变量与非静态变量的区别

The Java® Language Specification Java SE 8 Edition

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值