澳门新葡萄京官网注册 1

澳门新葡萄京官网注册超详细的Java面试题总结(一)之Java基本知识

无继承

最近在备战面试的过程中,整理一下面试题。大多数题目都是自己手敲的,网上也有很多这样的总结。自己感觉总是很乱,所以花了很久把自己觉得重要的东西总结了一下。

从栈帧说起

澳门新葡萄京官网注册 1

方法-栈帧的组成

之前说道每个方法的栈帧包括了局部变量表,操作数栈,方法出口等信息。

有 static 修饰

面向对象和面向过程的区别

面向过程

澳门新葡萄京官网注册 ,优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。

缺点:没有面向对象易维护、易复用、易扩展

面向对象

优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护

缺点:性能比面向过程低

Java语言有哪些特点?

1,简单易学;2,面向对象(封装,继承,多态);3,平台无关性(Java虚拟机实现平台无关性);4,可靠性;5,安全性;6,支持多线程(C++语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而Java语言却提供了多线程支持);7,支持网络编程并且很方便(Java语言诞生本身就是为简化网络编程设计的,因此Java语言不仅支持网络编程而且很方便);8,编译与解释并存;

什么是字节码?采用字节码的最大好处是什么?什么Java是虚拟机?

先看下java中的编译器和解释器:

Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(即扩展名为.class的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行,这就是上面提到的Java的特点的编译与解释并存的解释。

Java源代码—->编译器—->jvm可执行的Java字节码(即虚拟指令)—->jvm—->jvm中解释器—–>机器可执行的二进制机器码—->程序运行。

局部变量表

前面提到java执行每个方法的时候会有一个栈帧,栈帧有包括了局部变量表和操作数栈(operand
stack)。
其中局部变量表记录了所有方法运行过程中用到的变量,包括:

  • this变量
  • 方法参数
  • 局部变量
    对于类方法(也就是static方法)来说,方法参数是从第0个位置开始的,而对于实例方法来说,第0个位置上的变量是this指针。
    局部变量可以包括如下的属性:
  • char
  • byte
  • boolean
  • int
  • short
  • long
  • float
  • double
  • 引用
  • 返回地址
    在32位虚拟机上除了long和double类型外,每个变量都只占局部变量区中的一个变量槽(slot),而long及double会占用两个连续的变量槽,因为这些类型是64位的。

static final

// 生成随机数字和字母,
public static final String getStringRandomFinal(int length) {
    String val = "";
    Random random = new Random();
    // 参数length,表示生成几位随机数
    for (int i = 0; i < length; i++) {
        String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num";
        // 输出字母还是数字
        if ("char".equalsIgnoreCase(charOrNum)) {
            // 输出是大写字母还是小写字母
            // int temp = random.nextInt(2) % 2 == 0 ? 65 : 97;
            val += (char) (random.nextInt(26) + 97);
        } else if ("num".equalsIgnoreCase(charOrNum)) {
            val += String.valueOf(random.nextInt(10));
        }
    }
    return val;
}

采用字节码的好处:

Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。

什么是Java虚拟机

任何一种可以运行Java字节码的软件均可看成是Java的虚拟机(JVM)

什么是Java程序的主类?应用程序和小程序的主类有何不同?

一个程序中可以有多个类,但只能有一个类是主类。在Java应用程序中,这个主类是指包含main()方法的类。而在Java小程序中,这个主类是一个继承自系统类JApplet或Applet的子类。应用程序的主类不一定要求是public类,但小程序的主类要求必须是public类。主类是Java程序执行的入口点。

什么是JDK?什么是JRE?

JDK:顾名思义它是给开发者提供的开发工具箱,是给程序开发者用的。它除了包括完整的JRE(Java
Runtime Environment),Java运行环境,还包含了其他供开发者使用的工具包。

JDK

JRE:普通用户而只需要安装JRE(Java Runtime Environment)来
来运行Java程序。而程序开发者必须安装JDK来编译、调试程序。

环境变量Path和ClassPath的作用是什么?如何设置这两个环境变量?

Java环境变量PATH和CLASSPATH – 简书 www.jianshu.com/p/d63b099cf…

Java应用程序与小程序之间有那些差别?

简单说应用程序是从主线程启动(也就是main()方法)。applet小程序没有main方法,主要是嵌在浏览器页面上运行(调用init()线程或者run()来启动),嵌入浏览器这点跟flash的小游戏类似。

字符型常量和字符串常量的区别

1) 形式上:

字符常量是单引号引起的一个字符

字符串常量是双引号引起的若干个字符

2) 含义上:

字符常量相当于一个整形值(ASCII值),可以参加表达式运算

字符串常量代表一个地址值(该字符串在内存中存放位置)

3) 占内存大小

字符常量只占一个字节

字符串常量占若干个字节(至少一个字符结束标志)

Java语言采用何种编码方案?有何特点?

Java语言采用Unicode编码标准,Unicode(标准码),它为每个字符制订了一个唯一的数值,因此在任何的语言,平台,程序都可以放心的使用。

构造器Constructor是否可被override

在讲继承的时候我们就知道父类的私有属性和构造方法并不能被继承,所以Constructor也就不能被override,但是可以overload,所以你可以看到一个类中有多个构造函数的情况。

重载和重写的区别

重载:发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。

重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类;如果父类方法访问修饰符为private则子类中就不是重写。

java
面向对象三大特性(封装,继承,多态)以及抽象、接口的介绍,访问控制符public,protected,private,以及默认的区别

www.2cto.com/kf/201605/5…

java中equals方法的用法以及==的用法

www.cnblogs.com/bluestorm/a…

String和StringBuffer、StringBuilder的区别是什么?String为什么是不可变的?

可变性

String类中使用字符数组保存字符串,private final char value[],所以string对象是不可变的。StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,char[]value,这两种对象都是可变的。

线程安全性

String中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。

性能

每次对String
类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String
对象。StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用StirngBuilder
相比使用StringBuffer 仅能获得10%~15%
左右的性能提升,但却要冒多线程不安全的风险。

对于三者使用的总结:

如果要操作少量的数据用 = String

单线程操作字符串缓冲区 下操作大量数据 = StringBuilder

多线程操作字符串缓冲区 下操作大量数据 = StringBuffer

自动装箱与拆箱

装箱:将基本类型用它们对应的引用类型包装起来;

拆箱:将包装类型转换为基本数据类型;

Java使用自动装箱和拆箱机制,节省了常用数值的内存开销和创建对象的开销,提高了效率,由编译器来完成,编译器会在编译期根据语法决定是否进行装箱和拆箱动作。

类、方法、成员变量和局部变量的可用修饰符 –

blog.csdn.net/yttcjj/arti…

在一个静态方法内调用一个非静态成员为什么是非法的?

由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。

在Java中定义一个不做事且没有参数的构造方法的作用

Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。

import java和javax有什么区别

www.cnblogs.com/EasonJim/p/…

接口和抽象类的区别是什么?

1.接口的方法默认是public,所有方法在接口中不能有实现,抽象类可以有非抽象的方法

2.接口中的实例变量默认是final类型的,而抽象类中则不一定

3.一个类可以实现多个接口,但最多只能实现一个抽象类

4.一个类实现接口的话要实现接口的所有方法,而抽象类不一定

5.接口不能用new实例化,但可以声明,但是必须引用一个实现该接口的对象

从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。

成员变量与局部变量的区别有那些?

从语法形式上,看成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被public,private,static等修饰符所修饰,而局部变量不能被访问控制修饰符及static所修饰;成员变量和局部变量都能被final所修饰;

从变量在内存中的存储方式来看,成员变量是对象的一部分,而对象存在于堆内存,局部变量存在于栈内存

从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。

成员变量如果没有被赋初值,则会自动以类型的默认值而赋值(一种情况例外被final修饰但没有被static修饰的成员变量必须显示地赋值);而局部变量则不会自动赋值。

创建一个对象用什么运算符?对象实体与对象引用有何不同?

new运算符,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指向它(可以用n条绳子系住一个气球)

什么是方法的返回值?返回值在类的方法里的作用是什么?

方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作用:接收出结果,使得它可以用于其他的操作!

一个类的构造方法的作用是什么?若一个类没有声明构造方法,改程序能正确执行吗?为什么?

主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。

构造方法有哪些特性?

1,名字与类名相同;2,没有返回值,但不能用void声明构造函数;3,生成类的对象时自动执行,无需调用。

静态方法和实例方法有何不同?

静态方法和实例方法的区别主要体现在两个方面:

在外部调用静态方法时,可以使用”类名.方法名”的方式,也可以使用”对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。

静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制

对象的相等与指向他们的引用相等,两者有什么不同?

对象的相等 比的是内存中存放的内容是否相等而 引用相等
比较的是他们指向的内存地址是否相等。

在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?

帮助子类做初始化工作。

什么是多态机制?Java语言是如何实现多态的?

常见的变量赋值

当一个新的变量创建的时候,操作数栈会用来存储这个新变量的值。然后这个变量会存储到局部变量区中对应的位置上。如果这个变量不是基础类型的话,本地变量槽上存的就只是一个引用。这个引用指向堆的里一个对象。
比如int i = 5; 字节码对应了:

0: iconst_5
2: istore_0

每一个指令的解释如下:

操作指令 解释
iconst_5 用来将一个字节作为整型数字压入操作数栈中,在这里5就会被压入操作数栈上。
istore_0 这是istore_这组指令集(译注:严格来说,这个应该叫做操作码,opcode ,指令是指操作码加上对应的操作数,oprand。不过操作码一般作为指令的助记符,这里统称为指令)中的一条,这组指令是将一个整型数字存储到本地变量中。n代表的是局部变量区中的位置,并且只能是0,1,2,3。再多的话只能用另一条指令istore了,这条指令会接受一个操作数,对应的是局部变量区中的位置信息。

但是如果换成如下语句 int i = 6; 则对应了

0: bipush 6
2: istore_0

造成这种原因的差别源于字节码指令集的定义:iconst<n>指令(将常量加载到操作栈)只包含了0-5,大于5则会使用bipush。

同时还有一点需要注意的是,在class文件中,被初始化的变量操作都会加到构造方法里面。

  public com.ys.testmodel.Loo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: bipush        6
         7: putfield      #2                  // Field a:I
        10: return
}

相关指令的解释如下:

操作指令 解释
aload_0 从局部变量数组中加载一个对象引用到操作数栈的栈顶。尽管这段代码看起来没有构造方法,但是在编译器生成的默认的构造方法里,就会包含这段初始化的代码。第一个局部变量正好是this引用,于是aload_0把this引用压到操作数栈中。aload_0是aload_指令集中的一条,这组指令会将引用加载到操作数栈中。n对应的是局部变量数组中的位置,并且也只能是0,1,2,3。还有类似的加载指令,它们加载的并不是对象引用,比如iload_,lload_,fload_,和dload_, 这里i代表int,l代表long,f代表float,d代表double。局部变量的在数组中的位置大于3的,得通过iload,lload,fload,dload,和aload进行加载,这些指令都接受一个操作数,它代表的是要加载的局部变量的在数组中的位置。
invokespecial 这条指令可以用来调用对象实例的构造方法,私有方法和父类中的方法。它是方法调用指令集中的一条,其它的还有invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual.这里的invokespecial指令调用的是父类也就是java.lang.Object的构造方法。
bipush 它是用来把一个字节作为整型压到操作数栈中的,在这里6会被压到操作数栈里。
putfield 它接受一个操作数,这个操作数引用的是运行时常量池里的一个字段,在这里这个字段是simpleField。赋给这个字段的值,以及包含这个字段的对象引用,在执行这条指令的时候,都 会从操作数栈顶上pop出来。前面的aload_0指令已经把包含这个字段的对象压到操作数栈上了,而后面的bipush又把6压到栈里。最后putfield指令会将这两个值从栈顶弹出。执行完的结果就是这个对象的simpleField这个字段的值更新成了6。

这里的putfield指令的操作数引用的是常量池里的第二个位置。JVM会为每个类型维护一个常量池,运行时的数据结构有点类似一个符号表,尽管它包含的信息更多。Java中的字节码操作需要对应的数据,但通常这些数据都太大了,存储在字节码里不适合,它们会被存储在常量池里面,而字节码包含一个常量池里的引用
。当类文件生成的时候,其中的一块就是常量池:

Constant pool:
   #1 = Methodref          #4.#10         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#11         // com/ys/testmodel/Loo.a:I
   #3 = Class              #12            // com/ys/testmodel/Loo
   #4 = Class              #13            // java/lang/Object
   #5 = Utf8               a
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = NameAndType        #7:#8          // "<init>":()V
  #11 = NameAndType        #5:#6          // a:I
  #12 = Utf8               com/ys/testmodel/Loo
  #13 = Utf8               java/lang/Object

static 非 final

// 生成随机数字和字母,
public static String getStringRandom(int length) {
    String val = "";
    Random random = new Random();
    // 参数length,表示生成几位随机数
    for (int i = 0; i < length; i++) {
        String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num";
        // 输出字母还是数字
        if ("char".equalsIgnoreCase(charOrNum)) {
            // 输出是大写字母还是小写字母
            // int temp = random.nextInt(2) % 2 == 0 ? 65 : 97;
            val += (char) (random.nextInt(26) + 97);
        } else if ("num".equalsIgnoreCase(charOrNum)) {
            val += String.valueOf(random.nextInt(10));
        }
    }
    return val;
}
Flag

上面的变量flas是空的,因为代码中没有增加修饰符

  int a;
    descriptor: I
    flags:

final修饰符
如果写成如下形式

    public final int a = 6;

则对应的字节码为

  public final int a;
    descriptor: I
    flags: ACC_PUBLIC, ACC_FINAL
    ConstantValue: int 6

static修饰符

public static int a = 6;

对应的字节码为

  public static int a;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC

同时初始化代码并不在构造方法里面,而是转移到静态代码块

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: bipush        6
         2: putstatic     #2                  // Field a:I
         5: return

结果

这里使用了 OpenJDK 的 JMH 基准测试工具来测试的,结果如下:

# JMH 1.4.1 (released 903 days ago, please consider updating!)
# VM invoker: /srv/jdk1.8.0_92/jre/bin/java
# VM options: <none>
# Warmup: 20 iterations, 1 s each
# Measurement: 20 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.agoncal.sample.jmh.Main.benchmark
中间忽略了预热及测试过程,这里只显示结果
Result: 206924.113 ±(99.9%) 7746.446 ops/s [Average]
  Statistics: (min, avg, max) = (132107.466, 206924.113, 267265.397), stdev = 32798.937
  Confidence interval (99.9%): [199177.667, 214670.559]
# JMH 1.4.1 (released 903 days ago, please consider updating!)
# VM invoker: /srv/jdk1.8.0_92/jre/bin/java
# VM options: <none>
# Warmup: 20 iterations, 1 s each
# Measurement: 20 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.agoncal.sample.jmh.Main.benchmarkFinal
中间忽略了预热及测试过程,这里只显示结果
Result: 210111.568 ±(99.9%) 8486.176 ops/s [Average]
  Statistics: (min, avg, max) = (133813.368, 210111.568, 267525.228), stdev = 35931.001
  Confidence interval (99.9%): [201625.392, 218597.744]

# Run complete. Total time: 00:13:54
Benchmark                       Mode  Samples       Score      Error  Units
o.a.s.j.Main.benchmark         thrpt      200  206924.113 ± 7746.446  ops/s
o.a.s.j.Main.benchmarkFinal    thrpt      200  210111.568 ± 8486.176  ops/s

总结:你说final的性能比非final有没有提升呢?可以说有,但几乎可以忽略不计。如果单纯地追求性能,而将所有的方法修改为
final
的话,我认为这样子是不可取的。而且这性能的差别,远远也没有网上有些人说的提升
50% 这么恐怖(有可能他们使用的是10年前的JVM来测试的吧^_^,比如 《35+ 个
Java 代码性能优化总结》这篇文章。雷总:不服?咱们来跑个分!)

字节码指令集合

下面简单罗列了字节码指令集
加载和存储指令:

将一个局部变量加载到操作栈的指令包括有:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>
将一个数值从操作数栈存储到局部变量表的指令包括有:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>
将一个常量加载到操作数栈的指令包括有:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>

运算指令

加法指令:iadd、ladd、fadd、dadd
减法指令:isub、lsub、fsub、dsub
乘法指令:imul、lmul、fmul、dmul
除法指令:idiv、ldiv、fdiv、ddiv
求余指令:irem、lrem、frem、drem
取反指令:ineg、lneg、fneg、dneg
位移指令:ishl、ishr、iushr、lshl、lshr、lushr
按位或指令:ior、lor
按位与指令:iand、land
按位异或指令:ixor、lxor
局部变量自增指令:iinc
比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp

类型转换指令:

Java虚拟机对于宽化类型转换直接支持,并不需要指令执行,包括:
int类型到long、float或者double类型
long类型到float、double类型
float类型到double类型
窄化类型转换指令包括有:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。但是窄化类型转换很可能会造成精度丢失

对象创建与操作指令:

创建类实例的指令:new
创建数组的指令:newarray,anewarray,multianewarray

访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者成为实例变量)的指令:

getfield、putfield、getstatic、putstatic
把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
将一个操作数栈的值储存到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore

取数组长度的指令:arraylength

检查类实例类型的指令:
instanceof、checkcast

操作数栈管理指令:

Java虚拟机提供了一些用于直接操作操作数栈的指令,包括:pop、pop2、dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2和swap;

控制转移指令:

条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt, if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。
复合条件分支:tableswitch、lookupswitch
无条件分支:goto、goto_w、jsr、jsr_w、ret

方法调用和返回指令:

invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法(§2.9)、私有方法和父类方法。
invokestatic指令用于调用类方法(static方法)。

而方法返回指令则是根据返回值的类型区分的,包括有ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用

抛出异常指令:
athrow

分析

字节码级别的差别

StringKit.java
StringKitFinal.java

它们在字节码上的差别:

[18:52:08] emacsist:target $ diff /tmp/stringkit.log /tmp/stringkit-final.log
1,5c1,5
< Classfile /Users/emacsist/Documents/idea/logging/target/classes/org/agoncal/sample/jmh/StringKit.class
<   Last modified 2017-6-15; size 1098 bytes
<   MD5 checksum fe1ccdde26107e4037afc54c780f2c95
<   Compiled from "StringKit.java"
< public class org.agoncal.sample.jmh.StringKit
---
> Classfile /Users/emacsist/Documents/idea/logging/target/classes/org/agoncal/sample/jmh/StringKitFinal.class
>   Last modified 2017-6-15; size 1118 bytes
>   MD5 checksum 410f8bf0eb723b794e4754c6eb8b9829
>   Compiled from "StringKitFinal.java"
> public class org.agoncal.sample.jmh.StringKitFinal
24c24
<   #15 = Class              #52            // org/agoncal/sample/jmh/StringKit
---
>   #15 = Class              #52            // org/agoncal/sample/jmh/StringKitFinal
32,33c32,33
<   #23 = Utf8               Lorg/agoncal/sample/jmh/StringKit;
<   #24 = Utf8               getStringRandom
---
>   #23 = Utf8               Lorg/agoncal/sample/jmh/StringKitFinal;
>   #24 = Utf8               getStringRandomFinal
47c47
<   #38 = Utf8               StringKit.java
---
>   #38 = Utf8               StringKitFinal.java
61c61
<   #52 = Utf8               org/agoncal/sample/jmh/StringKit
---
>   #52 = Utf8               org/agoncal/sample/jmh/StringKitFinal
75c75
<   public org.agoncal.sample.jmh.StringKit();
---
>   public org.agoncal.sample.jmh.StringKitFinal();
87c87
<             0       5     0  this   Lorg/agoncal/sample/jmh/StringKit;
---
>             0       5     0  this   Lorg/agoncal/sample/jmh/StringKitFinal;
89c89
<   public static java.lang.String getStringRandom(int);
---
>   public static final java.lang.String getStringRandomFinal(int);
91c91
<     flags: ACC_PUBLIC, ACC_STATIC
---
>     flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
187c187
< SourceFile: "StringKit.java"
---
> SourceFile: "StringKitFinal.java"

可以看到除了方法名和方法修饰符不同之外,其他的没有什么区别了。

在调用者上面的字节码差别

public void benchmark();
  descriptor: ()V
  flags: ACC_PUBLIC
  Code:
    stack=1, locals=1, args_size=1
       0: bipush        32
       2: invokestatic  #2                  // Method org/agoncal/sample/jmh/StringKit.getStringRandom:(I)Ljava/lang/String;
       5: pop
       6: return
    LineNumberTable:
      line 21: 0
      line 22: 6
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       7     0  this   Lorg/agoncal/sample/jmh/Main;
  RuntimeVisibleAnnotations:
    0: #26()
public void benchmarkFinal();
  descriptor: ()V
  flags: ACC_PUBLIC
  Code:
    stack=1, locals=1, args_size=1
       0: bipush        32
       2: invokestatic  #3                  // Method org/agoncal/sample/jmh/StringKitFinal.getStringRandomFinal:(I)Ljava/lang/String;
       5: pop
       6: return
    LineNumberTable:
      line 26: 0
      line 27: 6
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       7     0  this   Lorg/agoncal/sample/jmh/Main;
  RuntimeVisibleAnnotations:
    0: #26()

可以看到,它们在调用者上面的字节码也没有什么区别,只是方法名不一样之外。

对于 JVM
来说,它是只认字节码的,既然字节码除了方法名和修饰符一样,其他都一样,那就可以大概推测它们的性能几乎可以忽略不计了。因为调用
static final 和 static 非 final 的JVM指令是一样。

无 static 修饰

方法体是一样的,只是将它们删除了 static 的修饰。

结果

# JMH version: 1.19
# VM version: JDK 1.8.0_92, VM 25.92-b14
# VM invoker: /srv/jdk1.8.0_92/jre/bin/java
# VM options: <none>
# Warmup: 20 iterations, 1 s each
# Measurement: 20 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.agoncal.sample.jmh.Main.benchmark
中间忽略了预热及测试过程,这里只显示结果
Result "org.agoncal.sample.jmh.Main.benchmark":
  201306.770 ±(99.9%) 8184.423 ops/s [Average]
  (min, avg, max) = (131889.934, 201306.770, 259928.172), stdev = 34653.361
  CI (99.9%): [193122.347, 209491.193] (assumes normal distribution)
# JMH version: 1.19
# VM version: JDK 1.8.0_92, VM 25.92-b14
# VM invoker: /srv/jdk1.8.0_92/jre/bin/java
# VM options: <none>
# Warmup: 20 iterations, 1 s each
# Measurement: 20 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.agoncal.sample.jmh.Main.benchmarkFinal
中间忽略了预热及测试过程,这里只显示结果
Result "org.agoncal.sample.jmh.Main.benchmarkFinal":
  196871.022 ±(99.9%) 8595.719 ops/s [Average]
  (min, avg, max) = (131182.268, 196871.022, 265522.769), stdev = 36394.814
  CI (99.9%): [188275.302, 205466.741] (assumes normal distribution)

# Run complete. Total time: 00:13:35
Benchmark             Mode  Cnt       Score      Error  Units
Main.benchmark       thrpt  200  201306.770 ± 8184.423  ops/s
Main.benchmarkFinal  thrpt  200  196871.022 ± 8595.719  ops/s

分析

字节码级别的差别

[19:20:17] emacsist:target $ diff /tmp/stringkit.log /tmp/stringkit-final.log
1,5c1,5
< Classfile /Users/emacsist/Documents/idea/logging/target/classes/org/agoncal/sample/jmh/StringKit.class
<   Last modified 2017-6-15; size 1110 bytes
<   MD5 checksum f61144e86f7c17dc5d5f2b2d35fac36d
<   Compiled from "StringKit.java"
< public class org.agoncal.sample.jmh.StringKit
---
> Classfile /Users/emacsist/Documents/idea/logging/target/classes/org/agoncal/sample/jmh/StringKitFinal.class
>   Last modified 2017-6-15; size 1130 bytes
>   MD5 checksum 15ce17ee17fdb5f4721f0921977b1e69
>   Compiled from "StringKitFinal.java"
> public class org.agoncal.sample.jmh.StringKitFinal
24c24
<   #15 = Class              #52            // org/agoncal/sample/jmh/StringKit
---
>   #15 = Class              #52            // org/agoncal/sample/jmh/StringKitFinal
32,33c32,33
<   #23 = Utf8               Lorg/agoncal/sample/jmh/StringKit;
<   #24 = Utf8               getStringRandom
---
>   #23 = Utf8               Lorg/agoncal/sample/jmh/StringKitFinal;
>   #24 = Utf8               getStringRandomFinal
47c47
<   #38 = Utf8               StringKit.java
---
>   #38 = Utf8               StringKitFinal.java
61c61
<   #52 = Utf8               org/agoncal/sample/jmh/StringKit
---
>   #52 = Utf8               org/agoncal/sample/jmh/StringKitFinal
75c75
<   public org.agoncal.sample.jmh.StringKit();
---
>   public org.agoncal.sample.jmh.StringKitFinal();
87c87
<             0       5     0  this   Lorg/agoncal/sample/jmh/StringKit;
---
>             0       5     0  this   Lorg/agoncal/sample/jmh/StringKitFinal;
89c89
<   public java.lang.String getStringRandom(int);
---
>   public final java.lang.String getStringRandomFinal(int);
91c91
<     flags: ACC_PUBLIC
---
>     flags: ACC_PUBLIC, ACC_FINAL
169c169
<             0     125     0  this   Lorg/agoncal/sample/jmh/StringKit;
---
>             0     125     0  this   Lorg/agoncal/sample/jmh/StringKitFinal;
188c188
< SourceFile: "StringKit.java"
---
> SourceFile: "StringKitFinal.java"

可以看到,字节码上除了名字和 final 修饰符差别外,其余的是一样的。

在调用者上面的字节码差别

public void benchmark();
  descriptor: ()V
  flags: ACC_PUBLIC
  Code:
    stack=2, locals=1, args_size=1
       0: new           #2                  // class org/agoncal/sample/jmh/StringKit
       3: dup
       4: invokespecial #3                  // Method org/agoncal/sample/jmh/StringKit."<init>":()V
       7: bipush        32
       9: invokevirtual #4                  // Method org/agoncal/sample/jmh/StringKit.getStringRandom:(I)Ljava/lang/String;
      12: pop
      13: return
    LineNumberTable:
      line 21: 0
      line 22: 13
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0      14     0  this   Lorg/agoncal/sample/jmh/Main;
  RuntimeVisibleAnnotations:
    0: #30()
public void benchmarkFinal();
  descriptor: ()V
  flags: ACC_PUBLIC
  Code:
    stack=2, locals=1, args_size=1
       0: new           #5                  // class org/agoncal/sample/jmh/StringKitFinal
       3: dup
       4: invokespecial #6                  // Method org/agoncal/sample/jmh/StringKitFinal."<init>":()V
       7: bipush        32
       9: invokevirtual #7                  // Method org/agoncal/sample/jmh/StringKitFinal.getStringRandomFinal:(I)Ljava/lang/String;
      12: pop
      13: return
    LineNumberTable:
      line 26: 0
      line 27: 13
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0      14     0  this   Lorg/agoncal/sample/jmh/Main;
  RuntimeVisibleAnnotations:
    0: #30()

可以看到,它们除了名字不同之外,其他的JVM指令都是一样的。

总结

对于是否有 final
修饰的方法,对性能的影响可以忽略不计。因为它们生成的字节码除了 flags
标志位是否有 final
修饰不同之外,其他所有的JVM指令,都是一样的(对于方法本身,以及调用者本身的字节码都一样)。对于JVM来说,它执行的就是字节码,如果字节码都一样的话,那对于JVM来说,它就是同一样东西的了。

有继承

无 final 修饰

package org.agoncal.sample.jmh;
import java.util.Random;
/**
 * Created by emacsist on 2017/6/15.
 */
public abstract class StringKitAbs {
    // 生成随机数字和字母,
    public String getStringRandom(int length) {
        String val = "";
        Random random = new Random();
        // 参数length,表示生成几位随机数
        for (int i = 0; i < length; i++) {
            String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num";
            // 输出字母还是数字
            if ("char".equalsIgnoreCase(charOrNum)) {
                // 输出是大写字母还是小写字母
                // int temp = random.nextInt(2) % 2 == 0 ? 65 : 97;
                val += (char) (random.nextInt(26) + 97);
            } else if ("num".equalsIgnoreCase(charOrNum)) {
                val += String.valueOf(random.nextInt(10));
            }
        }
        return val;
    }
}

有 final 修饰

package org.agoncal.sample.jmh;
import java.util.Random;
/**
 * Created by emacsist on 2017/6/15.
 */
public abstract class StringKitAbsFinal {
    // 生成随机数字和字母,
    public final String getStringRandomFinal(int length) {
        String val = "";
        Random random = new Random();
        // 参数length,表示生成几位随机数
        for (int i = 0; i < length; i++) {
            String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num";
            // 输出字母还是数字
            if ("char".equalsIgnoreCase(charOrNum)) {
                // 输出是大写字母还是小写字母
                // int temp = random.nextInt(2) % 2 == 0 ? 65 : 97;
                val += (char) (random.nextInt(26) + 97);
            } else if ("num".equalsIgnoreCase(charOrNum)) {
                val += String.valueOf(random.nextInt(10));
            }
        }
        return val;
    }
}

测试代码

写一个类来继承上面的抽象类,以此来测试在继承中 final 有否对多态中的影响

package org.agoncal.sample.jmh;
/**
 * Created by emacsist on 2017/6/15.
 */
public class StringKitFinal extends StringKitAbsFinal {
}

package org.agoncal.sample.jmh;
/**
 * Created by emacsist on 2017/6/15.
 */
public class StringKit extends StringKitAbs {
}

然后在基准测试中:

@Benchmark
public void benchmark() {
    new StringKit().getStringRandom(32);
}
@Benchmark
public void benchmarkFinal() {
    new StringKitFinal().getStringRandomFinal(32);
}

测试结果

非 final 结果

# JMH version: 1.19
# VM version: JDK 1.8.0_92, VM 25.92-b14
# VM invoker: /srv/jdk1.8.0_92/jre/bin/java
# VM options: <none>
# Warmup: 20 iterations, 1 s each
# Measurement: 20 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.agoncal.sample.jmh.Main.benchmark
中间忽略了预热及测试过程
Result "org.agoncal.sample.jmh.Main.benchmark":
  213462.677 ±(99.9%) 8670.164 ops/s [Average]
  (min, avg, max) = (135751.428, 213462.677, 264182.887), stdev = 36710.017
  CI (99.9%): [204792.513, 222132.841] (assumes normal distribution)

有 final 结果

# JMH version: 1.19
# VM version: JDK 1.8.0_92, VM 25.92-b14
# VM invoker: /srv/jdk1.8.0_92/jre/bin/java
# VM options: <none>
# Warmup: 20 iterations, 1 s each
# Measurement: 20 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.agoncal.sample.jmh.Main.benchmarkFinal
中间忽略了预热及测试过程
Result "org.agoncal.sample.jmh.Main.benchmarkFinal":
  213684.585 ±(99.9%) 8571.512 ops/s [Average]
  (min, avg, max) = (133472.162, 213684.585, 267742.236), stdev = 36292.318
  CI (99.9%): [205113.073, 222256.097] (assumes normal distribution)

总对比

# Run complete. Total time: 00:13:35
Benchmark             Mode  Cnt       Score      Error  Units
Main.benchmark       thrpt  200  213462.677 ± 8670.164  ops/s
Main.benchmarkFinal  thrpt  200  213684.585 ± 8571.512  ops/s

它们字节码的区别

[12:12:19] emacsist:classes $ diff /tmp/StringKit.log /tmp/StringKitFinal.log
1,5c1,5
< Classfile /Users/emacsist/Documents/idea/logging/target/classes/org/agoncal/sample/jmh/StringKit.class
<   Last modified 2017-6-16; size 317 bytes
<   MD5 checksum 7f9b024adc7f39345215e3e8490cafe4
<   Compiled from "StringKit.java"
< public class org.agoncal.sample.jmh.StringKit extends org.agoncal.sample.jmh.StringKitAbs
---
> Classfile /Users/emacsist/Documents/idea/logging/target/classes/org/agoncal/sample/jmh/StringKitFinal.class
>   Last modified 2017-6-16; size 337 bytes
>   MD5 checksum f54eadc79a90675d97e95f766ef88a87
>   Compiled from "StringKitFinal.java"
> public class org.agoncal.sample.jmh.StringKitFinal extends org.agoncal.sample.jmh.StringKitAbsFinal
10,12c10,12
<    #1 = Methodref          #3.#13         // org/agoncal/sample/jmh/StringKitAbs."<init>":()V
<    #2 = Class              #14            // org/agoncal/sample/jmh/StringKit
<    #3 = Class              #15            // org/agoncal/sample/jmh/StringKitAbs
---
>    #1 = Methodref          #3.#13         // org/agoncal/sample/jmh/StringKitAbsFinal."<init>":()V
>    #2 = Class              #14            // org/agoncal/sample/jmh/StringKitFinal
>    #3 = Class              #15            // org/agoncal/sample/jmh/StringKitAbsFinal
19c19
<   #10 = Utf8               Lorg/agoncal/sample/jmh/StringKit;
---
>   #10 = Utf8               Lorg/agoncal/sample/jmh/StringKitFinal;
21c21
<   #12 = Utf8               StringKit.java
---
>   #12 = Utf8               StringKitFinal.java
23,24c23,24
<   #14 = Utf8               org/agoncal/sample/jmh/StringKit
<   #15 = Utf8               org/agoncal/sample/jmh/StringKitAbs
---
>   #14 = Utf8               org/agoncal/sample/jmh/StringKitFinal
>   #15 = Utf8               org/agoncal/sample/jmh/StringKitAbsFinal
26c26
<   public org.agoncal.sample.jmh.StringKit();
---
>   public org.agoncal.sample.jmh.StringKitFinal();
32c32
<          1: invokespecial #1                  // Method org/agoncal/sample/jmh/StringKitAbs."<init>":()V
---
>          1: invokespecial #1                  // Method org/agoncal/sample/jmh/StringKitAbsFinal."<init>":()V
38c38
<             0       5     0  this   Lorg/agoncal/sample/jmh/StringKit;
---
>             0       5     0  this   Lorg/agoncal/sample/jmh/StringKitFinal;
40c40
< SourceFile: "StringKit.java"
---
> SourceFile: "StringKitFinal.java"

可以看到,除了它们的方法签名和方法名字不同之外其他的都是一样的,包括JVM调用指令也完全是一样的。

总结

可以看到它们几乎是一样的。

总结

基于上面的基准测试结论,我认为滥用或刻意为了所谓的提升性能,而去为每一个方法尽可能添加
final 的关键字是不可取的。使用 final ,更多的应该是根据Java对 final
的语义来定义,而不是只想着为了提升性能(而且这影响可以忽略不计)而刻意用
final.

使用 final 的情况:

final 变量: 表示只读(只初始化一次,但可多次读取)
final 方法:表示子类不可以重写。(网上认为 final 比非 final
快,就是认为它是在编译的时候已经静态绑定了,不需要在运行时再动态绑定。这个可能以前的JVM上是正确的,但在现代的JVM上,这个可以认为没什么影响,至少我在基准测试里是这样子)
final 类: 它们不能被继承,而且final类的方法,默认也是 final 的。

关于这个 final 的性能问题,我也Google了下,发现 stackoverflow
上,也有类似的问题:stackoverflow

发表评论

电子邮件地址不会被公开。 必填项已用*标注