JAVA修仙升级一本通

举报
huahua.Dr 发表于 2023/10/27 17:08:27 2023/10/27
【摘要】 一、基础1.基础数据类型及应用1.1 基本类型             数据类型默认值大小(字节=8位)声明正负边界byte0    1byte a = 100-2^7 /2^7-1short    0    2short s = 1000-2^15 /2^15-1int    0    4int a = 100000-2^31 /2^31-1long    0L    8long a = ...

一、基础

1.基础数据类型及应用

1.1 基本类型             

数据类型 默认值 大小(字节=8位) 声明 正负边界
byte 0     1 byte a = 100 -2^7 /2^7-1
short     0     2 short s = 1000 -2^15 /2^15-1
int     0     4 int a = 100000 -2^31 /2^31-1
long     0L     8 long a = 1000L -2^63 /2^63-1
float     0.0f     4     float f = 234.5f
double     0.0d     8 double d=0.2d
char     ‘u0000’  2

char letter = 'A'

char letter = '\u0000' 

\u0000/ \uffff(65535)
String     null     –       

boolean     false     1
byte、int、long、和short都可以用十进制、16进制以及8进制的方式来表示,当使用字面量的时候,前缀 0 表示 8 进制,而前缀 0x 代表 16 进制, 例如:
int decimal = 100;
int octal = 0144;
int hexa =  0x64;
long 和 float 类型初始化需要标识符

1.2 类型转换

(1)转换规则

  • 低  ->  高 :byte,short,char—> int —> long—> float —> double

  • 在把容量大的类型转换为容量小的类型时必须使用强制类型转换

  • 浮点数到整数的转换是通过舍弃小数得到,而不是四舍五入

(2)计算转换

short a = 1;

short b = 2;

那么 a+b 是什么类型?

答:在java的世界里,如果比int类型小的类型做运算,java在编译的时候就会将它们统一强转成int类型。当是比int类型大的类型做运算,就会自动转换成它们中最大类型那个。

所有的byte,short,char型的值在操作时将被提升为int型

(3)强转符号的作用域

short s = 1;
s = (short) s + 1; // 错误,等于没处理
s = (short) (s + 1); // 正确
  • 低级到高级可以隐式转换,高级到低级必须显示转换。
  • +=, -= 类似 (short)(xx + xx),自带了强制转换。
  • 对于 short s1 = 1; s1 += 1;由于 += 是 java 语言规定的运算符,java 编译器会对它进行特殊处理,s1 = (short) (s1 + 1);因此 可以正确编译。
  • 对于 short s1 = 1; s1 = s1 + 1; 由于 s1+1 运算时会自动提升表达式的类型,所以结果是 int 型,再赋值给 short 类型 s1 时,编译器将报告需要强制转换类型的错误。

(4)浮点精度
  • 与0.0f或0.0d作算术比较(>或<判断)是允许的,但不能直接判断相等==。
  • 做大小比较时考虑使用Float或Double的compare (f1, f2)方法,或BigDecimal。
  • 整数型包装类型应该使用equals方法做比较。
  • 浮点型包装类型不能用equals或者flt.compareTo(another) == 0进行相等的比较(可以进行compareTo(another)的大小比较)。
  • 进行相等的比较:计算绝对值是否小于1e-6:(Math.abs(a -10.0) < 1e-6)。
  • 和NaN的大小比较 和Doube.NaN() 禁止进行比较,!= 一定返回true,其余一定返回false。

(5)常量符号的影响

final修饰的变量如何转型:被final修饰的变量不会自动改变类型,当2个final修饰相操作时,结果会根据左边变量的类型而转化


1.3  封装类型缓存

  • Integer 初始化-128~127的范围缓存。
  • Boolean 全局缓存。
  • Character,缓存范围'\u0000' 到 '\u007F'。
Integer i = 0;
Integer j = Integer.valueOf(0);
Integer k = Integer.valueOf("0");
Integer m = Integer.parseInt("0");
Integer n = new Integer(0);
 
System.out.println(i == j); // true
System.out.println(i == k); // true
System.out.println(i == m); // true
System.out.println(i == n); // false
 
Integer fst = 1;
Integer snd = new Integer(1);
 
System.out.println(fst == snd);//false
System.out.println(new Integer(1) == snd);//false
System.out.println(fst == Integer.valueOf(1));//true
System.out.println(fst == Integer.valueOf("1"));//true
System.out.println(fst == Integer.parseInt("1"));//true
 
Integer trd = 256;
System.out.println(trd == Integer.valueOf(256));//false
System.out.println(trd == Integer.valueOf("256"));//false
System.out.println(trd == Integer.parseInt("256"));//true parseInt 返回的是基础类型,基础类型可以使用`==`,比较的时候直接拆包了

1.4 字符串

(1)字符串缓存

  • String对象是不变对象,在程序运行过程中可能用到多个具有相同值得String对象,jvm中使用String pool来优化这种情况。当有新的String对象要建立的时候,jvm先检查Pool中时候已经有具有相同值的String对象,如果有就把这个对象的引用传递给新建立的对象,如果没有,就新建立一个对象,并将它放到Pool中。
  • intern() 方法(调用该方法返回一个字符串,内容与调用该方法的字符串的内容相同,但保证来源于缓冲池中,如果池中没有于该字符串相同的字符串就将该String对象放入池中,并且返回该对象的引用)
  • 如果左侧全是常量:对于常量,编译时就直接存储它们的字面值而不是它们的引用,在编译时就直接将它们连接的结果提取出来变成了”abc” ,该语句在class文件中就相当于String s = “abc”,然后当JVM执行到这一句的时候, 就在String pool(字符串缓冲池)里找,如果没有这个字符串,就会产生一个。如果左侧存在引用:
String s1 = "abc";
String s2 = new String( "abc" ); // 2 个
/***************/
String s1 = new String( "abc" ); // 2 个, 先创建1个作为构造函数的参数,再创建一个作为新的对象
/***************/
String s1 = "abc";
String s2 = new String( "abc" ).intern(); // 1个
/***************/
String str1 = "aaa"; 
String str2 = "bbb"; 
String str3 = "aaabbb"; 
String str4 = "aaa" + "bbb";//1个,不会产生新的字符串对象 
System.out.println(str3 == str4);//true   
str4 = str1 + "bbb";//会产生新的字符串对象 
System.out.println(str3 == str4);//false 
str4 = str1 + str2;//会产生新的字符串对象 
System.out.println(str3 == str4);//false 
/***************/
final String str1 = "aaa"; 
final String str2 = "bbb"; 
String str3 = "aaabbb"; 
 
/*
 * 因为str1与str2都定义成了常量,所以编译时就能确定,编译时就会将常量替换,等同于
 * str4 = "aaa"+"bbb",因此不产生新对象
 */ 
String str4 = str1 + str2; 
System.out.println(str3 == str4);//true 
练习题
1. char[]能存放哪些 ()
A. '\n'
B. "a"
C. '\u0012'
D. '\u12'
E. '我'
答案解析 ACE char类型:char类型占两个字节,而java默认采用Unicode码是16位,所以一个Unicode码占两个字节,java中无论汉字还是英文都是用Unicode编码来表达的,所以可以存储一个汉字

2. 精度最高的数据类型?
A. int
B. long
C.BigInteger
D.byte
答案解析 C 和long型整数运算比,BigInteger不会有范围限制,但缺点是速度比较慢。

3.哪些语句在编译时不会出现编译警告()
A) float f = 1.3; B) byte b=257;
C) boolean b = null; D) int i = 10;
答案解析D

4. 以下说法不正确的是:()
A、Integer i1 = 10; Integer i2 = 10; i1和i2指向同一个对象
B、整数型包装类型应使用equals做相等的比较
C、基本类型优于包装类型
D、浮点型包装类型建议使用equals或flt.compareTo(another)==0做相等的比较
答案解析 D、浮点类型无法做精确的 == 操作

5. public static void main(String[] args) {
    byte b1 = 1, b2 = 2, b3, b6, b8;
 
    final byte b4 = 4, b5 = 6, b7;
    b3 = b1 + b2;// 语句1
    b6 = b4 + b5; // 语句2
    b8 = b1 + b4; // 语句3
    b7 = b2 + b5; // 语句4
    System.out.println(b3 + b6);
}
上述代码片段中,存在编译错误的语句是()
A语句1      B语句2     C语句3     D语句4  
答案解析 ACD ;A 语句生成为int, 赋值给byte需要强转 B 正确 final 语句赋值根据 左侧byte 赋值 CD 含有一个非final 仍旧要求强转

6. 以下代码的执行结果:()
public static void main(String[] args) {
    char alpha = 'A';
    int foo = 65;
    boolean trueExp = true;
    System.out.println(trueExp ? alpha : 0);
    System.out.println(trueExp ? alpha : foo);
}
A、A A
B、65 65
C、A 65
D、65 A
答案解析 C trueExp ? alpha : 0 返回的是char trueExp ? alpha : foo 返回的是int
7. 以下代码输出什么 ()
List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);
System.out.println((String)arrayList.get(1));
 
A. 100
B. 编译错误
C. 运行异常
D. aaaa
答案解析 C Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

8. 如下类型,哪些是值不可变类型
 
A、char
B、Integer
C、String
D、short
答案解析BC 解析:值不可变类型:八个基本类型的包装类和String类

9. String str = “a” + “b” + “c” + “d”; 创建了几个对象?
A. 4个
B. 1个
C. 2个
D. 3个
答案解析B

2.引用数据类型及应用

3.java基本语法

3.1 可变参数

可变参数最后还是被编译器处理成了数组,参数列表中可以有0个或者多个参数
只能出现在参数列表的最后,…位于变量类型和变量名之间,前后有无空格都可以,调用可变参数的方法时,编译器为该可变参数隐含创建一个数组,在方法体中以数组的形式访问可变参数,如果一个是两个方法是重载,一个是可变参数,一个是固定参数,然后我们调用方法如果固定长度的能满足,那么有优先调用固定的长度的。

3.2 控制语句

(1)Switch
Switch 的行为
跳转到满足的语句分支,如果没有break,顺序执行结果

(2)条件
switch 不支持的数据类型:long boolean float double
switch 同时支持枚举类型,case语句中只能写枚举类定义的变量名称,不能加类。

3.3 循环

(1)异常循环
输出结果不确定,不能用浮点数作为循环变量:
class CodeSolution {
    public static void main(String[] args) {
        for (float flt = (float) 1000000000; flt < 1000000010; flt++) {
            System.out.println(flt);
        }
    }
}
溢出死循环:
i ++ 的条件 会强转成byte,溢出成为负数
class CodeSolution {
    public static void main(String[] args) {
        for (byte i = -128; i < 128; i++) {
            System.out.println(i);
        }
    }
}
如果是 i < end 结果为50次,但是 i = end 之后再加1就会溢出,导致死循环:
class CodeSolution {
    public static void main(String[] args) {
        int end = Integer.MAX_VALUE;
        int start = end - 50;
        int count = 0;
        for (int i = start; i <= end; i ++) {
            System.out.println( ++ count);
        }
    }
}
(2)循环条件
For的逗号操作符
java中唯一用到逗号操作符的地方就是for循环控制表达式,在控制表达式的初始化和步进控制部分,可以使用一系列由逗号分隔的语句;而且这些语句都会独立执行。

class CodeSolution {
    public static void main(String[] args) {
        for (byte i, pre = i = -128; i < 128 && i >= pre; pre = i, i++) {
            System.out.println(i);
            System.out.println(pre);
            System.out.println("---");
        }
    }
}
对于第一个语句 i = pre = 128,步长控制语句后 pre = -128, i = -127,相当于 i 始终大于pre 1
当i溢出以后,不满足大于pre的条件,所以本循环不会死循环

3.4  泛型

(1)extends 
<? extends T> 上界来接收返回的数据(取出来的类型向上强制转型为T,null 可以表示任何类型,所以null除外),不能使用 add 方法,适用频繁往外读取内容的
List<? extends Season> 表示 “具有任何从Season继承类型的列表”,编译器无法确定List所持有的类型,所以无法安全的向其中添加对象。可以添加null,因为null 可以表示任何类型。所以List 的add 方法不能添加任何有意义的元素,但是可以接受现有的子类型List 赋值。
 List<? extends Season> list1 = new ArrayList<Season>();
 List<? extends Season> list2 = new ArrayList<Spring>();
 List<? extends Season> list3 = new ArrayList<Winter>();
 List<Season> list4 = new ArrayList<Winter>(); // 编译错误

(2)super

<? super T>:是指 下界通配符,编译器只允许加入声明超类的子类,但是不许加入任何超类,但是可以接受现有的超类型List 赋值。
List<? super Apple> 表示 “Apple的父类对象组成的列表”,get禁用,因为无法确定get的类型。
List<Fruit> fruits = new ArrayList<Fruit>();
List<? super Apple> = fruits;
fruits.add(new Apple());                 //work
fruits.add(new RedApple());              //work
fruits.add(new Fruit());                 //compile error
fruits.add(new Object());                //compile error
class Food{}
class Friut extends Food{}
class Apple extends Friut{}
Food food = new Food();
Friut friut = new Friut();
Apple apple = new Apple();
List<? extends Friut> list1 = new ArrayList<>();
List<? super Friut> list2 = new ArrayList<>();
 
1、list1.add(apple); // false 不允许加入
2、List1.add(friut); // false
3、List1.add(food); // false
4、list2.add(apple); // true
5、List2.add(friut); // true
6、List2.add(food); // false 必须为子类
7、List<Food> foods = new ArrayList(); list1 = foods;  // list1 标识子类集合 false
8、List<Friut> foods = new ArrayList(); list1 = foods;  // true
9、List<Apple> foods = new ArrayList(); list1 = foods;  // true
10、List<Food> foods = new ArrayList(); list2 = foods;  // 父类集合 true
11、List<Friut> foods = new ArrayList(); list2 = foods;  // true
12、List<Apple> foods = new ArrayList(); list2 = foods;  // false

3.5 异常体系

  • Throwable是所有Error和Exception的超类。
  • 只有Throwable的子类才能被 catch 和 throw
  • Error类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢出,对于这类错误导致的引用程序中断,仅靠程序本身无法恢复和预防,遇到这样的错误建议让程序终止。
  • Exception类分为运行时异常和受检查异常,受检查异常要么用try catch捕获,要么用throws字句抛出给父类处理,负责编译报错。运行时异常都是RuntimeException类及其子类异常,如NullPointerException、IndexOutOfBoundsException等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。非运行时异常是RuntimeException以外的异常,类型上都属于Exception类及其子类。
(1)Exception 分类
对可容错处理(可恢复)的情况使用受检异常(checked exception),受检异常的特点在于它强制要求开发人员在代码中进行显式的声明和捕获,否则就会产生编译错误。对编程错误使用运行时异常(runtime exception)。
非受检的:NullPointerException,ClassCastException,IllegalArgumentException,ArrayIndexsOutOfBoundsException,ArithmeticException
受检:Exception,FileNotFoundException,IOException,SQLException
(2)异常行为
方法最多抛出异常数量:5个
行为:被第一个异常捕获,若有被对应的异常捕获,执行finally
异常的定义必须按层级结构来
class CodeSolution {
    public static void main(String[] args) {
        try{
            throw  new IllegalStateException("xx");
        } catch (IllegalStateException e){
            System.out.println("throw exception");
        } catch (Exception e){
            System.out.println("throw father exception");
        }
 
        try{
            throw  new IllegalStateException("xx");
        } catch (Exception e){
            System.out.println("throw father exception");
        } catch (IllegalStateException e){
            System.out.println("throw son exception"); // 编译失败
        }
    }
}
输出结果 345
class CodeSolution {
    public static void main(String[] args) {
        try {
            int num1 = 2;
            int num2 = 0;
            int result = num1 / num2;
            System.out.println(result);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("1");
        } catch (NumberFormatException e) {
            System.out.println("2");
        } catch (Exception e) {
            System.out.println("3");
        } finally {
            System.out.println("4");
        }
        System.out.println("5");
    }
}
如果finally中没有return语句,但是改变了要返回的值,这里有点类似与引用传递和值传递的区别,分以下两种情况

如果return的数据是基本数据类型或文本字符串,则在finally中对该基本数据的改变不起作用,try中的return语句依然会返回进入finally块之前保留的值。
如果return的数据是引用数据类型,而在finally中对该引用数据类型的属性值的改变起作用,try中的return语句返回的就是在finally中改变后的该属性的值
但是直接在finally语句中执行return会立即执行
throw和throws: throw是语句抛出的异常,throws是方法可能抛异常的声明。

class CodeSolution {
    public static void main(String[] args) {
        try {
            System.out.println("aa:" + func());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    public static int func() throws Exception {
        int elm = 1;
        for (int i = 1; i < 2; i++) {
            try {
                throw new Exception("bb");
            } catch (Exception ex) {
                throw ex;
            } finally {
                continue;//不能出现在finally中
            }
        }
        return 0;
    }
}
输出 aa:0
(3)敏感异常
  • java.io.FileNotFoundException 泄露文件系统结构和文件名列举
  • java.util.jar.JarException 泄露文件系统结构
  • java.util.MissingResourceException 资源列举
  • java.security.acl.NotOwnerException 所有人列举
  • java.util.ConcurrentModificationException 可能提供线程不安全的代码信息
  • javax.naming.InsufficientResourcesException 服务器资源不足(可能有利于DoS攻击)
  • java.net.BindException 当不信任客户端能够选择服务器端口时造成开放端口列举
  • java.lang.OutOfMemoryError DoS
  • java.lang.StackOverflowError DoS
  • java.sql.SQLException 数据库结构,用户名列举
练习题
1. 关于以下规则,说法正确的是:
1:case语句块结束时如果不加break,必须有注释说明 
2:switch语句必须有default分支  
3:变量应被声明在接近它们首次使用的行
4:每行仅能声明一个变量
答案解析
1:case语句块结束时如果不加break,必须有注释说明 // 对
2:switch语句必须有default分支 // 错误,除非条件为枚举
3:变量应被声明在接近它们首次使用的行 // 对
4:每行仅能声明一个变量 //错,for循环时允许多个的

2. 跳转语句不包括下列哪项:()
A:goto       B:continue       C:throw      D:return
答案解析 A

3. 以下关于泛型集合说法正确的是:()
A. `List<? extends T>`存放的类型一定为T及其子类,但是获取要用T或者其父类引用
B. `List<? extends T>`进行add(T子类)编译出错:因为无法确定到底是哪个子类
C. `List<? super T>` get()方法返回的对象都是Object类型,因为T的最上层父类是Object,想要向下转型只能强转
D. `List<? super T>`存放的类型一定为T及其父类,但添加一定为T和其子类对象
答案解析 ABD
方法的设计可优先考虑泛型
优先使用泛型集合,而不是数组
声明一个泛型类通过限定符限制可用的泛型类型

4. 以下说法正确的是()

A. Throwable是所有Error或Exception的超类。
B. 只有Throwable或其子类才能被抛出(throw)或者被捕获(catch)。
C. 绝大部分Error会导致程序本身(比如JVM)出现不可避免的、不可恢复的状态,所以这样的问题不需要在程序中进行处理。
D. 编译器会强制要求使用者捕获RuntimeException或申明抛出。
答案解析 ABC 说明: D、Checked异常需要显式处理,否则会编译出错;Runtime异常不需要显式处理或声明抛出

5.  在JAVA的异常处理中,用户自定义的异常类一般应该是()的子类
A. Error
B. Exception
C. RuntimeException
D. Throwable
答案解析B
6. 以下哪些属于非受检异常:
A. 变量指向空指针,返回NullPointerException
B. 超出数组范围
C. 变量访问了不存在的文件,返回FileNotFoundException
D. 读取文件的操作超出文档末尾范围
答案解析 AB
7. 以下哪些属于非受检异常:
A. ClassCastException
B. IndexOutOfBoundsException
C. NullPointerException
D.IllegalStateException
E.FileNotFoundException
F.NoSuchFieldException
答案解析ABCD
8.关于try-with-resources说法正确的是: (D)
A、不能加finally
B、按照创建的资源顺序关闭 // 逆序
C、try中创建的变量在catch和finally可见 // 不在同一作用域
D、资源关闭在catch和finally中语句执行之前

4.Lamda表达式

4.1 函数式接口:只有一个抽象方法的接口,存在以下规则:
 接口可以被继承,衍生出子类接口,也可以某个方法作为参数类型使用,调用时,会优先匹配子类的函数式接口,但如果存在多个子类函数式接口的方法,调用时只传函数式接口实例,则无法类型推断出具体使用哪个方法,编译会报错(只能强转成对应函数式接口类型);建议同一个函数式接口只继承一个子类,调用父类函数式接口类型的方法时只能使用强转

public class Main {
public static void main(String[] args) {
consume((MyFunction) Object::toString);
consume((Function<Object, String>) Object::toString);
consume(Object::toString);
consume(null);
}
private static void consume(Function<Object, String> mapping) {
System.out.println("jdk mapping");
}
private static void consume(MyFunction mapping) {
System.out.println("my mapping");
}
private static void consume(MyFunction2 mapping) {
System.out.println("my mapping2");
}
public interface MyFunction2 extends MyFunction {
}
public interface MyFunction extends Function<Object, String> {
}
}


输出:
my mapping
jdk mapping
my mapping2
my mapping2

5.IO编程

6.正则表达式

7.java面向对象特性

7.1 继承

  • 重写 (overload):子类对父类接口的重写,相同的返回值和形参 使用@override注解,用来检测是否是有效的正确覆写,但是不写不会编译时报错
  • 隐藏 (hide):父类同名的成员变量和静态方法只会被隐藏(静态绑定导致),父类的非静态方法会被覆盖(动态绑定导致)
  • 重载 (override):同类之间函数的不通返回值和形参
  • 遮蔽 (shadow): 一个变量、方法或类可以分别遮蔽(shadow)在类内部具有相同名字的变量、方法或类。如果一个实体被遮蔽了,name就无法通过简单名引用到它。

(1)不在父类的构造函数调用子类的覆写方法

public class SeniorClass {
    public SeniorClass() {
        toString();
    }
 
    @Override
    public String toString() {
        return "IAmSeniorClass";
    }
 
}
 
public class JuniorClass extends SeniorClass {
    private String name;
    public JuniorClass() {
        super();
        name = "JuniorClass";
    }
    @Override
    public String toString() {
        return name.toUpperCase();
    }
}
构造实例时导致NullPointerException

(2)父类引用指向子类

定义一个父类类型的引用指向一个子类的对象既可以使用子类强大的功能,又可以抽取父类的共性。
所以,父类类型的引用可以调用父类中定义的所有属性和方法,而对于子类中定义而父类中没有的方法,它是无可奈何的;同时,父类中的一个方法只有在在父类中定义而在子类中没有重写(不是重载)的情况下,才可以被父类类型的引用调用。

7.2 抽象类和接口

(1)抽象类

  • 可以有具体实现方法,抽象类可以有构造方法,接口中不能有构造方法
  • 抽象方法用abstract,抽象方法必须可以被继承 public或者protected
  • 如果一个类继承于一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为为abstract类
  • 不可以被实例化

(2)接口

  • 可有成员变量且一定是 public static final
  • 可有成员函数默认是 public abstract
  • 在java8以后,接口中可以添加使用default或者static修饰的方法。default修饰方法只能在接口中使用,在接口中被default标记的方法为普通方法,可以直接写方法体。

练习题

1. 子类不可以覆写父类的那些方法?
A. final
B. static
C. private
D. void
答案解析 ABC

2. 父类的哪个不能被子类隐藏:()
A:私有方法  B:属性  C:内部类  D:静态方法
答案解析 A

3. 
class Swan {
    public void fly() {
        System.out.println("swan can fly ...");
    }
 
    public static void flyS() {
        System.out.println("swan can flyS ...");
    }
}
 
class UglyDuck extends Swan {
    @Override
    public void fly() {
        System.out.println("ugly duck can't fly ...");
    }
 
    public static void flyS() {
        System.out.println("ugly duck can't flyS ...");
    }
}
 
class TestFly {
    public static void main(String[] args) {
        Swan swan = new Swan();
        Swan uglyDuck = new UglyDuck();
        swan.fly();
        uglyDuck.fly();
        swan.flyS();
        uglyDuck.flyS();
    }
}
输出:
答案解析
输出: swan can fly ... ugly duck can't fly ... swan can flyS ... swan can flyS ... 实例调用静态方法,相当于 Swan.flyS() 和子类无关

4.
class Father {
    private int year;
 
    public int getGood() {
        System.out.println("useFatherGet");
        return this.year;
    }
 
    public void setGood(int good) {
        this.year = good;
        System.out.println("useFatherSet");
    }
 
    @Override
    public String toString() {
        return this.year + "!!!";
    }
}
 
class Son extends Father {
    @Override
    public int getGood() {
        System.out.println("useSonGet");
        return super.getGood();
    }
 
    public int getGood(int good) {
        System.out.println("useSonGetOverWrite");
        return super.getGood();
    }
 
    @Override
    public void setGood(int good) {
        super.setGood(good);
        System.out.println("useSonSet");
    }
}
 
class CodeSolution {
    public static void main(String[] args) {
        Son son = new Son();
        son.setGood(11);
        System.out.println(son.toString());
    }
}
输出:
答案解析
输出: useFatherSet useSonSet 11!!! 实例调用父类方法,再调用自身方法

5. 
class CodeSolution {
    public static void main(String[] args) {
        Father father = new Son();
        father.setGood(32);
        System.out.println(father.toString());
    }
}
答案解析
输出: useFatherSet useSonSet 11!!! 父类可见子类重载的方法

6.
class CodeSolution {
    public static void main(String[] args) {
        Father father = new Son();
        father.setGood(32);
        father.getGood(32);
        System.out.println(father.toString());
    }
}
答案解析
编译错误,父类没有这个方法, 无法通过父类引用调用

7. 下列选项中,关于Java的抽象类和抽象方法说法正确的是:()
A、抽象类中可以含有0个或多个抽象方法
B、抽象类中不可以有构造方法
C、一个类中若有抽象方法,则这个类必为抽象类
D、子类必须重写父类所有的抽象方法
答案解析 ACD

8. 关于接口说法正确的是:()
A、接口中可包含静态方法和default方法
B、接口中属性默认public static final修饰词
C、方法已缺省具有public abstract修饰词
答案解析 ABC

9. 下列说法错误的有:()
A. 在类方法中可用this来调用本类的类方法 
B. 在类方法中调用本类的类方法时可直接调用
C. 在类方法中只能调用本类中的类方法
D. 在类方法中绝对不能调用实例方法  // main是例外
答案解析
ACD 类方法中不能出现this或者super关键字;在类方法中绝对不能调用实例方法除了main是例外

二、中级特性

1.java反射机制


2.java常用类库

2.1 集合

集合类的默认大小:ArrayList(10), Vector(10), StringBuilder(16), StringBuffer(16), HashMap(16), HashSet(16),HashTable(11)
动态扩容
  • ArrayList使用无参构造器创建的ArrayList初始容量为0(推荐),第一次调用add/addAll方法时才会初始化数组容量,初始容量为10。
  • 对集合添加若干元素时,如果当前集合的容量满足需求则不扩容,如果不满足则扩大1.5倍,如果扩大1.5依然不满足则扩大为满足需求的最小容量。
  • Vector、HashSet、HashMap 扩容增量:原容量的 1倍;ArrayList扩容增量:原容量的 0.5倍 + 1

2.2 哈希

HashMap HashTable主要在于以下几个方面不同点:
  • 父类不同:HashMap继承了AbstractMap,HashTable继承Dictionary抽象类,两者均实现Map接口;
  • Hashtable不允许null值(包括键或值),HashMap允许一个空键(其他的空键会覆盖第一个空键)和任意数量的空值;
  • Hashtable的方法是同步的,HashMap的方法不是同步的。这是两者最主要的区别;所以就意味着Hashtable是线程安全的,Hashtable效率较低;HashMap不是线程安全的,HashMap效率较高;
  • HashMap 存储原理:先比较hashcode。定位原理:先hashcode再equals

2.3 LinkedHashMap

关注点 结论
LinkedHashMap是否允许空  Key和Value都允许空
LinkedHashMap是否允许重复数据 Key重复会覆盖、Value允许重复
LinkedHashMap是否有序 有序
LinkedHashMap是否线程安全 非线程安全

    

2.4 子集合 SubList


1.该方法其实返回的是ArrayList的内部类SubList的一个实例,不能直接赋值:
ArrayList (ArrayList<String>) list.subList(0, 0);会报转换异常。
2.这个ArrayList.SubList内部类实例中的 parent 字段会持有外部类ArrayList对象的一个引用,同步修改

Arrays.asList Vs List.of
Arrays.asList returns a mutable list while the list returned by List.of is immutable
List<Integer> list = Arrays.asList(1, 2, null);
list.set(1, 10); // OK
 
List<Integer> list = List.of(1, 2, 3);
list.set(1, 10); // Fails with UnsupportedOperationException

Arrays.asList allows null elements while List.of doesn’t
List<Integer> list = Arrays.asList(1, 2, null); // OK
List<Integer> list = List.of(1, 2, null); // Fails with NullPointerException

Arrays.asList 作用于数组时,返回的是原数组的视图 List.of 则不是。
Integer[] array = {1,2,3};
List<Integer> list = Arrays.asList(array);
array[1] = 10;
System.out.println(list); // Prints [1, 10, 3]
 
Integer[] array = {1,2,3};
List<Integer> list = List.of(array);
array[1] = 10;
System.out.println(list); // Prints [1, 2, 3]
    
    

练习题:
1. 以下对Java集合说法正确的是:()
A. 往一个ArrayList或者Vector里插入一个元素时,如果内部数组空间不够,ArrayList或Vector会扩展它的大小。Vector在默认情况下增长一倍的大小,而ArrayList增加50%的大小
B. ArrayList、Vector、HashMap、StringBuilder和StringBuffer都是线程不安全的
C. Vector类实现了一个动态数组,默认不传参数时,构造容量为16的大小,也可以传参指定容量大小;ArrayList就是动态的数组,默认不传参数时,构造容量为10的大小,也可以传参指定容量大小
D. ConcurrentHashMap、HashMap、HashSet都不支持key为null,且遍历使用的都是Iterator迭代器,其中只有HashTable是线程安全的
答案解析 A

2. 如果一个对象存入Hash集合后hashcode随即发生变化,会导致什么结果? ()
A. 内存泄漏
B. 无任何问题
C. 内存溢出
D. 抛出异常
答案解析 A
3. 关于集合,以下说法正确的是
A、HashMap使用的是数组+链表(或红黑树)的方式,查找key时,先判断key的equals是否相等,相等时再判断hashCode是否一致
B、HashMap和HashSet的默认值大小为16,HashTable的默认值大小为11
C、LinkedHashMap保持插入的顺序,TreeMap保持key的自然顺序
D、Collection的直接子类包含Set、List、Map和Queue 
答案解析BC
HashMap中,先hashCode再进行equals equals() 方法只有在hashcode碰撞时才会被用到
B Vector是10 ArrayList是10 StringBuilder和StringBuffer都是16
D 没有Map

4. 
class CodeSolution {
    public static void main(String[] args) {
        Map<Integer, Integer> map = new LinkedHashMap<Integer, Integer>(16,0.75f,true){ // 排序模式accessOrder为true时(即按访问顺序排序)
            @Override
            protected boolean removeEldestEntry(Map.Entry eldest){
                return size() > 5;
            } // 插入之后调用此方法,返回true则删除最老的元素(最旧引用的)
        };
        for(int i = 0; i < 10; i++){
            map.put(i,i);
        }
        map.get(6);
        System.out.println(map.keySet());
    }
}
输出:[5, 7, 8, 9, 6]
如果参数为false 就是按插入顺序排序
5. 下面描述错误的是:
A、HashMap不允许null值(key和value都不可以),而Hashtable允许将null作为一个entry的key或者value
B、Hashtable是线程安全的,也就是说是同步的,HashMap线程不安全的,不是同步的
C、Hashtable和HashMap都实现了Map接口
D、ConcurrentHashMap不可存放key,value值都为null的数据
答案解析 A HashMap允许key和value是null值 Hashtable不允许

6. 下面描述正确的是 ()
A. 多线程环境下使用CourrentHashMap和Collections. synchronizedMap实现同步效率差别不大
B. Collections. synchronizedMap不是线程安全的
C. 多线程环境下使用HashTable和Collections. synchronizedMap实现同步效率差别不大
D. ConcurrentHashMap的锁粒度较大
答案解析
C Collections.synchronizedMap()和Hashtable一样,实现上在调用map所有方法时,都对整个map进行同步,而ConcurrentHashMap的实现却更加精细,它对map中的所有桶加了锁。ConcurrentHashMap在性能以及安全性方面,明显比Collections.synchronizedMap()更加有优势

7. 
List<String> tmpList = new LinkedList<>();
tmpList.add("Hello");
tmpList.add("My");
tmpList.add("Son");
 
for (String curStr : tmpList) {
if ("My".equals(curStr)) {
tmpList.remove(curStr);
}
System.out.printf("curStr = %s, tmpLIst = %s\n", curStr, tmpList.toString());
}
 
A.
curStr = Hello, tmpList = [Hello, My, Son]
curStr = My, tmpList = [Hello, Son]
 
B. 运行出错抛出IllegalStateException
C. 运行出错抛出ConcurrentModificationException
D.
curStr = Hello, tmpList = [Hello, My, Son]
curStr = My, tmpList = [Hello, Son]
curStr = Son, tmpList = [Hello, Son]
答案解析
循环删除 倒数第一 第二个数据时不会异常,所以为A

3.java线程同步


4.java线程规范与管理


三、高级特性

1.垃圾回收

必备知识:

1.1 垃圾回收是什么

(1)定义:从jvm内存管理角度看,把不用的内存回收掉,java采用自动内存管理技术,内存分配后由虚拟机自动管理。垃圾回收=垃圾识别+内存回收
(2)优点:程序员不需要自己释放内存,只管new对象即可
(3)缺点:GC本身有开销,会挤占业务执行资源。
(4)什么是垃圾:不会被访问到的对象是垃圾

1.2 如何识别垃圾

(1)使用引用计数法识别垃圾

原理:记录每个对象被引用的数量,当被引用的数量为0时,则标记为垃圾。
缺点:无法处理循环引用的问题
示例:
  • 对象A、B、C不是垃圾
  • 对象F是垃圾,引用计数为0,被回收
  • 对象D、E是垃圾,但引用计数不为0,出出现内存泄漏

(2)使用可达性分析

原理:从GC Roots开始遍历对象,没有被遍历到的对象为垃圾
GC Roots:
  • 方法栈使用到的参数、局部变量、临时变量等
  • 方法区中类静态属性引用的变量
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象
示例:
  • 对象A、B、C可以被遍历到,不是垃圾
  • 对象D、E不会被遍历到,会被回收
  • 目前主流虚拟机采用这种算法,包括Orace JDK、Huawei JDK等

1.3 如何执行垃圾回收(垃圾回收算法)

(1)清除(sweep)

原理:
将垃圾对象所占据的内存标记为空闲内存,然后存在一个空闲列表(free list)中。当需要创建对象时,从空闲列表中寻找空闲内存,分配给新创建的对象
优点:速度快
缺点:容易造成内存碎片,分配效率低

(2)整理(compact,也叫压缩)

原理
把存活的对象搬到内存的起始位置,然后在连续的空间内顺序分配
优缺点:
优点:分配速度快,局部性好
缺点:搬运对象麻烦,性能开销大

(3)复制(copy)

原理
将内存分为两个部分,并分别用from和to指针来维护。每次只在from指向的内存中分配内存,当发生垃圾回收时,将from指向区域中存活的对象复制到to指向的内存区域,然后将from指针和to指针互换位置。
优缺点:
优点:同压缩算法,没有内存碎片。分配速度快,局部性好
缺点:可用内存变少,堆空间使用效率低

1.4 有哪些垃圾收集器

按jvm堆划分 
jvm将堆划分为新生代和老年代。新生代存放新创建的对象,当对象生存超过一定时间时,会被移动至老年代。新生代采用的 GC 称为minor GC,老年代发生的 GC 称为 full GC 或 major GC,发生full GC会伴随至少一次minor GC。
Minor GC:
  • 特点:发生次数多,采用时间短,回收掉大量对象
  • 收集器:serial, Parallel Scavenge, Parallel New.均采用复制算法. Serial是单线程,Parallel New可以看成Serial多线程版本. Parallel Scanvenge和Parallel New类似,但更注重吞吐率,且不能与CMS一起使用
Full GC:
  • 特点:发生次数少,耗时长
  • 收集器:Serial Old(整理), Parallel Old(整理), CMS(清除). Serial Old是单线程的,Parallel Old可以看成Serial Old的多线程版本.  CMS是并发收集器,除了初始标记和重新标记操作需要Stop the world,其它时间可以与应用程序一起并发执行

1.5 垃圾回收的触发条件

Minor GC:Eden区空间不足
Full GC:
  • 老年代空间不足
  • 方法区(Metaspace)空间不足
  • 通过minor GC进入老年代的平均大小大于老年代的可用内存
  • 老年代被写满
  • 调用System.GC,系统建议执行full GC,但不一定执行。 禁止使用主动GC(除非在密码、RMI等方面),尤其是在频繁/周期性的逻辑中

2.类加载

2.1 类加载过程

加载:从不同数据源将java字节码读到jvm中,数据源包括zip压缩包,网络,运行时计算生成,其他文件生成,数据库等
简单来说,加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。
  • 支持的文件类型 jar | war | ear | (java web enterprise)
  • 字节码来源。一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译
  • 类加载器。一般包括启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。
注:为什么会有自定义类加载器?
一方面是由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。
另一方面也有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。

链接:
  • 验证:验证字节码信息是否符合jvm规范
  • 准备:分配内存,并为静态变量赋初始值
  • 解析:将常量池中的符号引用转换为直接引用。也可以在初始化之后再开始,来支持java的运行时绑定
初始化
  • 执行静态初始化块(static{})和类变量赋值.先初始化父类,后初始化子类
  • 不要在static块中抛出异常,否则会导致类初始化失败,抛ExceptionInInitializerError异常,进而导致其他异常
  • 对于静态变量、静态初始化块、变量、初始化块、构造器,它们的初始化顺序依次是(静态变量、静态初始化块)>(变量、初始化块)> 构造器。
    • 执行父类的静态代码块,并初始化父类静态成员变量
    • 执行子类的静态代码块,并初始化子类静态成员变量
    • 执行父类的构造代码块,执行父类的构造函数,并初始化父类普通成员变量
    • 执行子类的构造代码块, 执行子类的构造函数,并初始化子类普通成员变量

类的卸载

卸载类即该类的 Class 对象被 GC。
卸载类需要满足 3 个要求:
  • 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
  • 该类没有在其他任何地方被引用
  • 该类的类加载器的实例已被 GC
所以,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。

由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。 前面已经介绍过,Java虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。 Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。

2.2 ClassLoader层次结构

特点:
  • 双亲委派:先由父类加载器加载,加载不到或者加载失败后再由子类加载器加载
  • 如果Class文件不在父类的加载路径中,则由子类加载,如果仍然找不到,抛ClassNotFound异常
  • 先加载JDK中的类,再加载用户的类
双亲委派模式实现:

2.3 类初始化过程

时机:
  • JVM启动时,先初始化用户指定的主类
  • 初始化子类之前,先初始化父类
  • 访问类的静态变量或静态方法
  • 创建类实例
  • 反射调用类
特点:
  • JVM会加锁来保证类初始化只进行一次,
可以用来实现单例模式

2.4 对象初始化顺序

代码执行顺序:
  • 父类静态代码块
  • 子类静态代码块
  • 父类代码块
  • 父类构造函数
  • 子类代码块
  • 子类构造函数

2.5 JNI

有些事情Java无法处理时,JNI允许程序员用其他编程语言来解决,例如,Java标准库不支持的平台相关功能或者程序库。也用于改造已存在的用其它语言写的程序,供Java程序调用。许多基于JNI的标准库提供了很多功能给程序员使用,例如文件I/O、音频相关的功能。当然,也有各种高性能的程序,以及平台相关的API实现,允许所有Java应用程序安全并且平台独立地使用这些功能。JNI框架允许Native方法调用Java对象,就像Java程序访问Native对象一样方便。Native方法可以创建Java对象,读取这些对象,并调用Java对象执行某些方法。当然Native方法也可以读取由Java程序自身创建的对象,并调用这些对象的方法。

// 本地方法的正确定义方式
class HelloWorld {
public native void displayHelloWorld();
  static {
    System.loadLibrary("hello");
  }
  public static void main(String[] args) {
    new HelloWorld().displayHelloWorld();
  }
}

练习题

1.Java类加载表述正确的是()
A URLClassLoader的签名校验依赖jar包里的公钥,因此不能使用URLClassLoader来校验jar的合法性;
B 在JVM中将不同的ClassLoader实例加载的同一个类视为相同的类;
C 开发者不可以自定义类加载器
D. 当自定义一个类加载器时,为了保证赋予权限的完整,应该直接覆写getPermissions()方法,无需调用基类的getPermission()方法
答案解析
选A: 避免使用URLClassLoader和java.util.jar提供的自动签名检查机制;JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。
类加载器
启动类加载器(BootStrap ClassLoader)负责加载java核心类库即JDK\jre\lib路径下的类,如System,String等。此加载器比较特殊,不是java.lang.ClassLoader的子类,它本身是有C\C++实现,并不是java实现的。
扩展类加载器(Extension ClassLoader)负责加载扩展点类,即JDK\jre\lib\ext路径下的类。可以通过将自己开发的类存放在此路径下扩展核心类以外的新功能
系统加载器\程序应用加载器(System\Application ClassLoader)是加载CLASSPATH环境变量锁指定的jar包。一般情况用户自定义的类就是通过此加载器载入的。
自定义类加载器
2. 当一个自定义的类装载器要覆写getPermissions()方法的时候,如果没有调用父类的getPermissions方法来获取默认的系统规则,则()
A. 该类自定义的权限不会生效。
B. 该自定义类加载器加载的类具有的权限就会完全独立于系统全局策略文件规定的权限
C. 除了自定义策略外,系统全局的默认安全策略也被应用。
D. 该类的权限覆盖了这些系统全局文件规定的权限。
答案解析 BD 自定义类加载器如果忽略调用父类的getPermissions() 方法,该类加载器可以加载提升权限的不可信类 该自定义类加载器加载的类具有的权限就会完全独立于系统全局策略文件规定的权限

3. java编译与优化

Java的执行过程整体可以分为两个部分,第一步由javac将源码编译成字节码,在这个过程中会进行词法分析、语法分析、语义分析,编译原理中这部分的编译称为前端编译。第二步把字节码编译成机器码,但不是所有的代码都会被编译,只有被JVM认定为的热点代码,才可能被编译。

3.1 javac编译器

(1) javac执行过程:将.java源文件编译成.class字节码文件(前端编译)
(2)词法分析:将源文件编译成Token流

(3)语法分析

(4)语义分析

3.2 java即时编译器(后端编译)

(1)为什么需要JIT即时编译器

常见的编译型语言如C++,通常会把代码直接编译成CPU所能理解的机器码来运行。而Java为了实现“一次编译,处处运行”的特性,把编译的过程分成两部分,首先它会先由javac编译成通用的中间形式——字节码,然后再由解释器逐条将字节码解释为机器码来执行。所以在性能上,Java通常不如C++这类编译型语言。为了优化Java的性能 ,JVM在解释器之外引入了即时(Just In Time)编译器:当程序运行时,解释器首先发挥作用,代码可以直接执行。随着时间推移,即时编译器逐渐发挥作用,把越来越多的代码编译优化成本地代码,来获取更高的执行效率。解释器这时可以作为编译运行的降级手段,在一些不可靠的编译优化出现问题时,再切换回解释执行,保证程序可以正常运行。
即时编译器极大地提高了Java程序的运行速度,而且跟静态编译相比,即时编译器可以选择性地编译热点代码,省去了很多编译时间,也节省很多的空间。目前,即时编译器已经非常成熟了,在性能层面甚至可以和编译型语言相比。不过在这个领域,大家依然在不断探索如何结合不同的编译方式,使用更加智能的手段来提升程序的运行速度。

(2)执行过程

将javac编译的字节码放到解析器中执行,在解释执行的过程中,虚拟机同时对程序运行的信息进行收集,在这些信息的基础上,编译器会逐渐发挥作用,它会进行后端编译——把字节码编译成机器码,但不是所有的代码都会被编译,只有被JVM认定为的热点代码,才可能被编译。
(3)如何识别热点代码
JVM中会设置一个阈值,当方法或者代码块的在一定时间内的调用次数超过这个阈值时就会被编译,存入codeCache中。当下次执行时,再遇到这段代码,就会从codeCache中读取机器码,直接执行,以此来提升程序运行的性能。

(4)JIT即时编译器类型

JVM中集成了两种编译器,Client Compiler和Server Compiler,它们的作用也不同。Client Compiler注重启动速度和局部的优化,Server Compiler则更加关注全局的优化,性能会更好,但由于会进行更多的全局分析,所以启动速度会变慢。两种编译器有着不同的应用场景,在虚拟机中同时发挥作用。只打开C1: -XX:TieredStopAtLevel=1;只打开C2: -XX:-TieredCompilation

Client Compiler(C1编译器)

HotSpot VM带有一个Client Compiler C1编译器。这种编译器启动速度快,但是性能比较Server Compiler来说会差一些。C1会做三件事:
    • 局部简单可靠的优化,比如字节码上进行的一些基础优化,方法内联、常量传播等,放弃许多耗时较长的全局优化。
    • 将字节码构造成高级中间表示(High-level Intermediate Representation,以下称为HIR),HIR与平台无关,通常采用图结构,更适合JVM对程序进行优化。
    • 最后将HIR转换成低级中间表示(Low-level Intermediate Representation,以下称为LIR),在LIR的基础上会进行寄存器分配、窥孔优化(局部的优化方式,编译器在一个基本块或者多个基本块中,针对已经生成的代码,结合CPU自己指令的特点,通过一些认为可能带来性能提升的转换规则或者通过整体的分析,进行指令转换,来提升代码性能)等操作,最终生成机器码。

Server Compiler

Server Compiler主要关注一些编译耗时较长的全局优化,甚至会还会根据程序运行的信息进行一些不可靠的激进优化。这种编译器的启动时间长,适用于长时间运行的后台程序,它的性能通常比Client Compiler高30%以上。目前,Hotspot虚拟机中使用的Server Compiler有两种:C2和Graal。

C2编译器

   在Hotspot VM中,默认的Server Compiler是C2编译器。C2编译器在进行编译优化时,会使用一种控制流与数据流结合的图数据结构,称为Ideal Graph。 Ideal Graph表示当前程序的数据流向和指令间的依赖关系,依靠这种图结构,某些优化步骤(尤其是涉及浮动代码块的那些优化步骤)变得不那么复杂。
Ideal Graph的构建是在解析字节码的时候,根据字节码中的指令向一个空的Graph中添加节点,Graph中的节点通常对应一个指令块,每个指令块包含多条相关联的指令,JVM会利用一些优化技术对这些指令进行优化,比如Global Value Numbering、常量折叠等,解析结束后,还会进行一些死代码剔除的操作。生成Ideal Graph后,会在这个基础上结合收集的程序运行信息来进行一些全局的优化,这个阶段如果JVM判断此时没有全局优化的必要,就会跳过这部分优化。
无论是否进行全局优化,Ideal Graph都会被转化为一种更接近机器层面的MachNode Graph,最后编译的机器码就是从MachNode Graph中得的,生成机器码前还会有一些包括寄存器分配、窥孔优化等操作。关于Ideal Graph和各种全局的优化手段会在后面的章节详细介绍。Server Compiler编译优化的过程如下图所示:

Graal编译器
从JDK 9开始,Hotspot VM中集成了一种新的Server Compiler,Graal编译器。相比C2编译器,Graal有这样几种关键特性:
前文有提到,JVM会在解释执行的时候收集程序运行的各种信息,然后编译器会根据这些信息进行一些基于预测的激进优化,比如分支预测,根据程序不同分支的运行概率,选择性地编译一些概率较大的分支。Graal比C2更加青睐这种优化,所以Graal的峰值性能通常要比C2更好。
使用Java编写,对于Java语言,尤其是新特性,比如Lambda、Stream等更加友好。
更深层次的优化,比如虚函数的内联、部分逃逸分析等。
Graal编译器可以通过Java虚拟机参数-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler启用。当启用时,它将替换掉HotSpot中的C2编译器,并响应原本由C2负责的编译请求。

(5)JIT编译器如何执行:分层编译

在Java 7以前,需要研发人员根据服务的性质去选择编译器。对于需要快速启动的,或者一些不会长期运行的服务,可以采用编译效率较高的C1,对应参数-client。长期运行的服务,或者对峰值性能有要求的后台服务,可以采用峰值性能更好的C2,对应参数-server。Java 7开始引入了分层编译的概念,它结合了C1和C2的优势,追求启动速度和峰值性能的一个平衡。分层编译将JVM的执行状态分为了五个层次。五个层级分别是:
  • 0.解释执行。
  • 1.执行不带profiling的C1代码。
  • 2.执行仅带方法调用次数以及循环回边执行次数profiling的C1代码。
  • 3.执行带所有profiling的C1代码。
  • 4.执行C2代码。
profiling就是收集能够反映程序执行状态的数据。其中最基本的统计数据就是方法的调用次数,以及循环回边的执行次数。
通常情况下,C2代码的执行效率要比C1代码的高出30%以上。C1层执行的代码,按执行效率排序从高至低则是1层>2层>3层。这5个层次中,1层和4层都是终止状态,当一个方法到达终止状态后,只要编译后的代码并没有失效,那么JVM就不会再次发出该方法的编译请求的。服务实际运行时,JVM会根据服务运行情况,从解释执行开始,选择不同的编译路径,直到到达终止状态。下图中就列举了几种常见的编译路径:
  • 图中第①条路径,代表编译的一般情况,热点方法从解释执行到被3层的C1编译,最后被4层的C2编译。
  • 如果方法比较小(比如Java服务中常见的getter/setter方法),3层的profiling没有收集到有价值的数据,JVM就会断定该方法对于C1代码和C2代码的执行效率相同,就会执行图中第②条路径。在这种情况下,JVM会在3层编译之后,放弃进入C2编译,直接选择用1层的C1编译运行。
  • 在C1忙碌的情况下,执行图中第③条路径,在解释执行过程中对程序进行profiling ,根据信息直接由第4层的C2编译。
  • 前文提到C1中的执行效率是1层>2层>3层,第3层一般要比第2层慢35%以上,所以在C2忙碌的情况下,执行图中第④条路径。这时方法会被2层的C1编译,然后再被3层的C1编译,以减少方法在3层的执行时间。
  • 如果编译器做了一些比较激进的优化,比如分支预测,在实际运行时发现预测出错,这时就会进行反优化,重新进入解释执行,图中第⑤条执行路径代表的就是反优化。
总的来说,C1的编译速度更快,C2的编译质量更高,分层编译的不同编译路径,也就是JVM根据当前服务的运行情况来寻找当前服务的最佳平衡点的一个过程。从JDK 8开始,JVM默认开启分层编译。

(6)即时编译的触发时机

Java虚拟机根据方法的调用次数以及循环回边的执行次数来触发即时编译。
循环回边是一个控制流图中的概念,程序中可以简单理解为往回跳转的指令,比如下面这段代码:
上面这段代码经过编译生成下面的字节码。其中,偏移量为18的字节码将往回跳至偏移量为4的字节码中。在解释执行时,每当运行一次该指令,Java虚拟机便会将该方法的循环回边计数器加1。
在即时编译过程中,编译器会识别循环的头部和尾部。上面这段字节码中,循环体的头部和尾部分别为偏移量为11的字节码和偏移量为15的字节码。编译器将在循环体结尾增加循环回边计数器的代码,来对循环进行计数。
当方法的调用次数和循环回边的次数的和,超过由参数-XX:CompileThreshold指定的阈值时(使用C1时,默认值为1500;使用C2时,默认值为10000),就会触发即时编译。

开启分层编译的情况下,-XX:CompileThreshold参数设置的阈值将会失效,触发编译会由以下的条件来判断:

分层编译触发条件公式:i > TierXInvocationThreshold * s || (i > TierXMinInvocationThreshold * s  && i + b > TierXCompileThreshold * s) 
i为调用次数,b是循环回边次数
  • 方法调用次数大于由参数-XX:TierXInvocationThreshold指定的阈值乘以系数。
  • 方法调用次数大于由参数-XX:TierXMINInvocationThreshold指定的阈值乘以系数,并且方法调用次数和循环回边次数之和大于由参数-XX:TierXCompileThreshold指定的阈值乘以系数时。
上述满足其中一个条件就会触发即时编译,并且JVM会根据当前的编译方法数以及编译线程数动态调整系数s。

4.疑难问题定位

(1)JVM常用的调优命令

命令 说明
jps JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
命令格式
jps [options] [hostid]
option参数
-l : 输出主类全名或jar路径
-q : 只输出LVMID
-m : 输出JVM启动时传递给main()的参数
-v : 输出JVM启动时显示指定的JVM参数
其中[option]、[hostid]参数也可以不写。
jstat jstat(JVM statistics Monitoring)是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
命令格式
jstat [option] LVMID [interval] [count]
参数
[option] : 操作参数
LVMID : 本地虚拟机进程ID
[interval] : 连续输出的时间间隔
[count] : 连续输出的次数

option 参数总览
class    class loader的行为统计。Statistics on the behavior of the class loader.
compiler    HotSpt JIT编译器行为统计。Statistics of the behavior of the HotSpot Just-in-Time compiler.
gc    垃圾回收堆的行为统计。Statistics of the behavior of the garbage collected heap.
gccapacity    各个垃圾回收代容量(young,old,perm)和他们相应的空间统计。Statistics of the capacities of the generations and their corresponding spaces.
gcutil    垃圾回收统计概述。Summary of garbage collection statistics.
gccause    垃圾收集统计概述(同-gcutil),附加最近两次垃圾回收事件的原因。Summary of garbage collection statistics (same as -gcutil), with the cause of the last and
gcnew    新生代行为统计。Statistics of the behavior of the new generation.
gcnewcapacity    新生代与其相应的内存空间的统计。Statistics of the sizes of the new generations and its corresponding spaces.
gcold    年老代和永生代行为统计。Statistics of the behavior of the old and permanent generations.
gcoldcapacity    年老代行为统计。Statistics of the sizes of the old generation.
gcpermcapacity    永生代行为统计。Statistics of the sizes of the permanent generation.
printcompilation    HotSpot编译方法统计。HotSpot compilation method statistics.
jmap jmap(JVM Memory Map)命令用于生成heap dump文件,如果不使用这个命令,还阔以使用-XX:+HeapDumpOnOutOfMemoryError参数来让虚拟机出现OOM的时候·自动生成dump文件。
jmap不仅能生成dump文件,还阔以查询finalize执行队列、Java堆和永久代的详细信息,如当前使用率、当前使用的是哪种收集器等。

命令格式
jmap [option] LVMID
option参数
dump : 生成堆转储快照
finalizerinfo : 显示在F-Queue队列等待Finalizer线程执行finalizer方法的对象
heap : 显示Java堆详细信息
histo : 显示堆中对象的统计信息
permstat : to print permanent generation statistics
F : 当-dump没有响应时,强制生成dump快照
jhat jhat(JVM Heap Analysis Tool)命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看。在此要注意,一般不会直接在服务器上进行分析,因为jhat是一个耗时并且耗费硬件资源的过程,一般把服务器生成的dump文件复制到本地或其他机器上进行分析。
命令格式
jhat [dumpfile]
jstack

jstack用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。另外,jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。

命令格式
jstack [option] LVMID
option参数
-F : 当正常输出请求不被响应时,强制输出线程堆栈
-l : 除堆栈外,显示关于锁的附加信息
-m : 如果调用到本地方法的话,可以显示C/C++的堆栈
jinfo jinfo(JVM Configuration info)这个命令作用是实时查看和调整虚拟机运行参数。
之前的jps -v口令只能查看到显示指定的参数,如果想要查看未被显示指定的参数的值就要使用jinfo口令

命令格式
jinfo [option] [args] LVMID
option参数
-flag : 输出指定args参数的值
-flags : 不需要args参数,输出所有JVM参数的值
-sysprops : 输出系统属性,等同于System.getProperties()

四、实战应用

1.java安全管理器

  • 是一个定义安全策略的对象,此策略指定不安全或敏感操作。
  • 安全策略不允许的任何操作都会抛出SecurityException异常
  • Java应用程序默认不启用SecurityManager
  • 当SecurityManager检测到违反安全策略的操作时,将引用AccessControlExption或SecurityException
  • 不要将AllPermission许可覆给不可信的代码
在启用安全管理器的时候,配置遵循以下基本原则:

  • 没有配置的权限表示没有。
  • 只能配置有什么权限,不能配置禁止做什么。
  • 同一种权限可多次配置,取并集。
  • 同一资源的多种权限可用逗号分割。
练习题

1. 关于SecurityManager说法正确的是
A、可以用来控制加载某些类
B、可以用来控制能否读取系统参数
C、可以用来控制能否自己停止线程
D、可以用来控制监听端口
答案解析 ABCD

 2. grant permission java.util.PropertyPermission "java.version", "read" // 设置权限
  public static void main(String[] args) {
    String javaVersion=System.getProperty("java.version");
    System.err.println(javaVersion);
    System.setProperty("java.version","1.7.0_45");
    String javaNewVersion=System.getProperty("java.version");
    System.err.println(javaNewVersion);
 }
输出:
答案解析:java.securify.AccesscontroIException:access denied 权限不足

2.JDBC数据库开发


3.Socket网络编程


4.实践编程基本规范

常见基本规则
  • 方法 50 行 ;类 2000 行 都是非空非注释的
  • 嵌套不超过四层
  • 建议 构造方法如果参数较多,尽量重用
  • 建议 对于返回数组或者容器的方法,应返回长度为0的数组或者容器,代替返回null
  • 在最低限度,Javadoc用于每一个 public或 protected 修饰的类、接口、枚举、方法和成员变量
  • 变量命名不能与关键字冲突:public protected private abstract default static final transient volatile synchronized native strictfp

练习题:

1. Java编程规范,关于空行的用法?
A. 变量命名之间有空行
B. 方法之间有两个空行
C. 大括号行首有空行
D. 大括号行尾有空行
答案解析 A
减少不必要的空行,大括号行首和行尾之间不要有空行

2. 以下命名不推荐的是:()
A、某测试类方法命名:public void test_get_logic_Region_info() throws Exception { } 
B、某异常类命名:public class ServiceAccess {} 
C、某泛型类型变量命名:public class Box<T2> {}
D、某普通类命名:public class XMLService {}
答案解析 ABD
泛型类型变量 单个大写字母,可接一个数字,例如E, T, U,X, T2
public void test_get_logic_Region_info() throws Exception { } // 测试方法大小写混杂
public class ServiceAccess {} // ServiceAccessException
public class XMLService {} // public class XmlService {}

3. 以下说法正确的是:()
A、每行限长120个窄字符
B、每行不超过1个语句
C、避免文件过长,不超过1000行(非空非注释行)
D、一个源文件按顺序包含版权、package、import、顶层类,且用空行分隔
E、避免方法过长,不超过50行(非空非注释行)
答案解析 ABDE
 避免文件过长,不超过2000行(非空非注释行)

4.以下说法正确的是:()
A、换行起点在点号、双冒号、类型&、catch块中管道之前
B、换行起点在点号、冒号、类型&、catch块中管道之前
C、换行起点在方法左括号、逗号、lambda箭头和其左大括号之后
D、换行起点在方法右括号、逗号、lambda箭头和其右大括号之后
答案解析 AC

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。