Dialog Papers

1 Target-Guided Open-Domain Conversation(ACL 2019)

code: https://github.com/squareRoot3/Target-Guided-Conversation

  • motivation: we want a conversational system to chat naturally with human and proactively guide the conversation to a designated target subject

  • approach:
    1. propose a structured approach that introduces coarse-grained keywords to control the intended content of system responses.
    2. then attain smooth conversation transition through turn-level supervised learning, and drive the conversation towards the target with
      discourse-level constraints.
    3. further derive a keyword-augmented conversation dataset for the study.

divide-and-conquer approach

Turn-level Keyword Transition

this module aims to predict keywords of the next response that is appropriate in the conversation context. This part is agnostic to the end target,and therefore aligns with the conventional chitchat objective. We thus can use any open-ended chat data with extracted utterance keywords to
learn the prediction module in a supervised manner.

Pairwise PMI-based Transition

given two keywords wi and wj , computes likeliness of wj → wi:

PMI(wi, wj ) = log p(wi|wj )/p(wi)

where p(wi|wj ) is the ratio of transitioning to wi in the next turn given wj in the current turn, and p(wi) is the ratio of wi occurrence.

  • The approach enjoys simplicity and interpretability, yet can suffer from data sparsity and perform poorly with a priori unseen transition pairs.

Neural-based Prediction

first use a recurrent network to encode the conversation history, and feed the resulting features to a prediction layer to obtain a distribution over keywords for the next turn.The network is learned by maximizing the likelihood of observed keywords in the data.

  • The neural approach is straightforward, but can rely on a large amount of data for learning.

Hybrid Kernel-based Method*

given a pair of a current keyword and a candidate next keyword, by first measuring the cosine similarity of their normalized word embeddings, and feeding the quantity to a kernel layer consisting of K RBF kernels.The output of the kernel layer is a K-dimension kernel feature vector, which is then fed to a single-unit dense layer for a candidate score.The score is finally normalized across all candidate keywords to yield the candidate probability distribution.

Discourse-level Target-Guided Strategy

This module aims to fulfill the end target by proactively driving the discussion topic forward in the course of the conversation.

  1. constrain that the keyword of each turn must move strictly closer to the end target compared to those of preceding turns.
  2. use cosine similarity between normalized word embeddings as the measure of keyword closeness.
  3. the above constraint first collects a set of valid candidates, and the turn-level transition module samples or picks the most likely one the from the set according to the keyword distribution.
  • the predicted keyword for next response can be both a smooth transition and an effective step towards the target.

Keyword-augmented Response Retrieval

The final module in the system aims to produce a response conditioning on both the conversation history and the predicted keyword.

  • retrieval-based approach* or generation-based method

Dataset

the PersonaChat corpus:

  • The conversations cover a broadrange of topics such as work, family, personal interest, etc; and the discussion topics change frequently during the course of the conversations.These properties make the conversations particularly suitable for learning smooth, natural transitions at each turn.
  • without end targets and do not exhibit target-guided behaviors

*apply a rule-based keyword extractor which combines TF-IDF and Part-Of Speech features for scoring word salience

Case Study

2 Target-Guided Open-Domain Conversation Planning (COLING 2022)

code:https://github.com/y-kishinami/TGCP

  • motivation: Prior studies addressing target-oriented conversational tasks lack a crucial notion that has been intensively studied in the context of goaloriented artificial intelligence agents, namely,planning

  • contributions:

  1. propose the TGCP task as a framework to assess the prerequisite ability of a model for goal-oriented conversation planning

  1. conduct a set of experiments on the TGCP framework using
    several existing retrieval-based neural models and recently proposed strong generative neural models of conversational agents

  1. Our experimental results reveal the challenges facing current technology

  • Using TGCP, we revealed that the dialogue models with current technology have difficulty planning conversations to achieve given goals while ensuring the naturalness of the conversation.
  • The experimental results also showed that refining the subgoal strategies for generative models might be an effective method to overcome this trade-off.

3 Conversational Graph Grounded Policy Learning for Open-Domain Conversation Generation(ACL 2020)

  • motivation: utterance-level methods tend to produce less coherent multi-turn dialogs since it is quite challenging to learn semantic transitions in a dialog flow merely from dialog data without the help of prior information
  • approach:propose to represent prior information about dialog transitions as a graph and learn a graph grounded dialog policy, aimed at fostering a more coherent and controllable dialog.
    1. a Conversational Graph (CG) that captures both localappropriateness and global-coherence information,
    2. a reinforcement learning (RL) based policy,model that learns to leverage the CG to foster a more coherent dialog.

Conversational Graph (CG)

use vertices to represent utterance content, and edges to represent dialog transitions between utterances:

  1. a what-vertex that contains a keyword
  2. a how-vertex that contains a responding mechanism (from a multi-mapping based generator) to capture rich variability of expressions
    also use this multi-mapping based method to build edges between two what-vertices to capture the local-appropriateness between the two keywords as a message and a response respectively

  • Given a user message, to obtain candidate actions, the NLU module attempts to retrieve contextually relevant subgraphs from CG.
  • The state/action module maintains candidate actions, history keywords that selected by policy at previous turns or mentioned by user, and the message.
  • The policy module learns to select a response keyword and a
    responding mechanism from the above subgraphs.
  • The NLG module first encodes the message into a representation using a message encoder and the selected mechanism, and then employs a Seq2BF
    model (Mou et al., 2016) to produce a response with the above representation and the selected keyword as input.

Background: Multi-mapping Generator for NLG

CG Construction

Given a dialog corpus D, we construct the CG with three steps:

  1. what-vertex construction:use a rule-based keyword extractor to obtain salient keywords from utterances in D
  2. how-vertex construction: a set of responding mechanisms from the generator
  3. edge construction:
    • One is to join two what-vertices:selecting top five keywords decoded (decoding length is 1) by each responding mechanism
    • the other is to join a what-vertex and a how-vertex:use the ground-truth response to select the most suitable mechanism for each keyword.select top five mechanisms that are frequently selected for vw’s keyword

NLU

To obtain subgraphs to provide high-quality candidate actions:

  1. extract keywords in the last utterance of the context (message) using the same tool in CG construction
  2. link each keyword to the CG through exact string matching, to obtain multiple hit what-vertices
  3. retrieve a subgraph for each keyword, and use vertices (exclude hit what-vertices) in these subgraphs as candidate actions

State/Action

This module maintains candidate actions, history keywords that selected by the policy or mentioned by user, and the message.

Policy

  • State representation: concatenate a message representation and a history keywords representation + a graph attention mechanism and graph embedding to encode global structure information into state representation
  • Policy decision:
    1. what-policy selects a what-vertex from candidate what-vertices
    2. how-policy selects a how-vertex from how-vertex neighbors of the selected what-vertex

Rewards

utterance-level rewards:

  1. Local relevance:DualEncoder in (Lowe et al., 2015)
  2. Repetition:Repetition penalty is 1 if the generated response shares more than 60% words with any contextual utterances, otherwise 0
  3. Target similarity:calculate cosine similarity between the chosen keyword and the target word in pretrained word embedding space as target similarity

To leverage the global graph structure information of CG to facilitate policy learning:

  1. Global coherence: the average cosine distance between the chosen what-vertex and one of history what-vertices (selected or mentioned previously) in TransE based embedding space
  2. Sustainability: calculate a PageRank score (calculated on the full CG) for the chosen whatvertex
  3. Shortest path distance to the target:closer 1;otherwise 0;not change -1

NLG

feed the keyword in the selected what-vertex and r¯ into a Seq2BF decoder (Mou et al., 2016) for response generation.

offer---Java

Java基础

Java程序编写和执行流程

  1. 编写。 .java结尾的源文件
  2. 编译。 对源文件编译成.class, “javac xxx.java”
  3. 运行。 “java 字节码文件名”

企业真题(一)

1.一个”.java”源文件中是否可以包括多个类?有什么限制(明*数据)

是!

一个源文件中可以声明多个类,但是最多只能有一个类使用public进行声明。
且要求声明为public的类的类名与源文件名相同。

2.Java 的优势(阿**巴)

  • 跨平台型
  • 安全性高
  • 简单性
  • 高性能
  • 面向对象性
  • 健壮性

3.常用的几个命令行操作都有哪些?(至少4个)(北京数字**)

4.Java 中是否存在内存溢出、内存泄漏?如何解决?举例说明(拼*多)

存在!

不能举例。

5. 如何看待Java是一门半编译半解释型的语言(携*)

关键字

特点:关键字都是小写(class,void,static,public)
‘const’ 和 ‘goto’ not used but served;true,false,null 不是关键字,为字面量

标识符

命名规则:

  • 由 26 个英文字母大小写,0-9 ,_或 $ 组成
  • 数字不可以开头。
  • 不可以使用关键字和保留字,但能包含关键字和保留字。
  • Java 中严格区分大小写,长度无限制。
  • 标识符不能包含空格。
    命名规范:
  • 包名:多单词组成时所有字母都小写:xxxyyyzzz。
    例如:java.lang、com.atguigu.bean
  • 类名、接口名:多单词组成时,所有单词的首字母大写:XxxYyyZzz
    例如:HelloWorld,String,System 等
  • 变量名、方法名:多单词组成时,第一个单词首字母小写,第二个单词开始每个单
    词首字母大写:xxxYyyZzz
    例如:age,name,bookName,main,binarySearch,getName
  • 常量名:所有字母都大写。多单词时每个单词用下划线连接:XXX_YYY_ZZZ
    例如:MAX_VALUE,PI,DEFAULT_CAPACITY

变量

注意:

  • Java 中每个变量必须先声明,后使用。
  • 使用变量名来访问这块区域的数据。
  • 变量的作用域:其定义所在的一对{ }内。
  • 变量只有在其作用域内才有效。出了作用域,变量不可以再被调用。
  • 同一个作用域内,不能定义重名的变量。
    基本数据类型:包括 整数类型、浮点数类型、字符类型、布尔类型。
    引用数据类型:包括数组、 类、接口、枚举、注解、记录。

基本数据类型

整数类型:byte、short、int、long

  • 定义 long 类型的变量,赋值时需要以”l”或”L”作为后缀。
  • Java 程序中变量通常声明为 int 型,除非不足以表示较大的数,才使用 long。
  • Java 的整型常量默认为 int 型。

浮点类型:float、double

  • float:尾数可以精确到 7 位有效数字
  • double:精度是 float 的两倍
  • 定义 float 类型的变量,赋值时需要以”f”或”F”作为后缀。
  • Java 的浮点型常量默认为 double 型。
    注意:
  1. 并不是所有的小数都能可以精确的用二进制浮点数表示。二进制浮点数不能精确的表
    示 0.1、0.01、0.001 这样 10 的负次幂。(为什么 0.1 + 0.2 不等于 0.3)
  2. 浮点类型 float、double 的数据不适合在不容许舍入误差的金融计算领域。如果需要精确数字计算或保留指定位数的精度,需要使用 BigDecimal 类。

字符类型:char

  • 形式 1:使用单引号(‘ ‘)括起来的单个字符。
  • 形式 2:直接使用 Unicode 值来表示字符型常量:‘\uXXXX’。其中,
    XXXX 代表一个十六进制整数。例如:\u0023 表示 ‘#’。
  • 形式 3:Java 中还允许使用转义字符‘\’来将其后的字符转变为特殊字符
    型常量。 如:’\n’表示换行符

布尔类型:boolean

  • boolean 类型用来判断逻辑条件,一般用于流程控制语句中
  • boolean 类型数据只有两个值:true、false,无其它;不可以使用 0 或非 0 的整数替代 false 和 true

基本数据类型变量间运算规则

自动类型提升

规则:将取值范围小(或容量小)的类型自动提升为取值范围大(或容量大)的类型 。

  • 当存储范围小的数据类型与存储范围大的数据类型变量一起混合运算时,会按照其中最大的类型运算
  • 当 byte,short,char 数据类型的变量进行算术运算时,按照 int 类型处理。

强制类型转换

规则:将取值范围大(或容量大)的类型强制转换成取值范围小(或容量小)的类型。
转换格式:数据类型 1 变量名 = (数据类型 1)被强转数据值; //()中的数据类型必须<=变量值的数据类型

  • 当把存储范围大的值(常量值、变量的值、表达式计算的结果值)强制转
    换为存储范围小的变量时,可能会损失精度或溢出

      double d = 1.2;
      int num = (int)d;//损失精度
      int i = 200;
      byte b = (byte)i;//溢出  
    
  • 声明 long 类型变量时,可以出现省略后缀的情况。float 则不同。

基本数据类型与 String 的运算

String 不是基本数据类型,属于引用数据类型

  1. 任意八种基本数据类型的数据与 String 类型只能进行连接“+”运算,且结果
    一定也是 String 类型
  2. String 类型不能通过强制类型()转换,转为其他的类型;借助包装类的方法
    才能转

常识:进制的认识

  • 熟悉:二进制(以0B、0b开头)、十进制、八进制(以0开头)、十六进制(以0x或0X开头)的声明方式。
  • 二进制的理解
    • 正数:原码、反码、补码三码合一。
    • 负数:原码、反码、补码不相同。了解三者之间的关系。
    • 计算机的底层是以补码的方式存储数据的。
  • 熟悉:二进制与十进制之间的转换
  • 了解:二进制与八进制、十六进制间的转换

运算符

算术运算符

+  -  +  -  *  /  %  (前)++  (后)++  (前)--  (后)--  +  

赋值运算符

= +=、 -=、*=、 /=、%=
① 当“=”两侧数据类型不一致时,可以使用自动类型转换或使用强制类型转换原则进行处理。
② 支持连续赋值。
③ +=、 -=、*=、 /=、%= 操作,不会改变变量本身的数据类型。

比较运算符

== != > < >= <= instanceof
① instanceof 在面向对象的多态性的位置讲解。
② == != > < >= <= 适用于基本数据类型。(细节:> < >= <=不适用于boolean类型)
运算的结果为boolean类型。
③ 了解: == != 可以适用于引用数据类型
④ 区分:== 与 =

逻辑运算符

  1. & && | || ! ^
  2. 说明:
    ① 逻辑运算符针对的都是boolean类型的变量进行的操作
    ② 逻辑运算符运算的结果也是boolean类型。
    ③ 逻辑运算符常使用条件判断结构、循环结构中
  • 区分:& 和 &&
    1、相同点:两个符号表达的都是”且”的关系。只有当符号左右两边的类型值均为true时,结果才为true。

2、执行过程:
1)如果符号左边是true,则& 、&& 都会执行符号右边的操作
2)如果符号左边是false,则 & 会继续执行符号右边的操作,&& 不会执行符号右边的操作
3、开发中,我们推荐使用&&

  • 区分:| 和 ||(与& 和 &&类似)

位运算符(了解)

  1. << >> >>> & | ^ ~

  2. 说明:

① << >> >>> & | ^ ~ :针对数值类型的变量或常量进行运算,运算的结果也是数值
② << : 在一定范围内,每向左移动一位,结果就在原有的基础上 * 2。(对于正数、负数都适用);>> : 在一定范围内,每向右移动一位,结果就在原有的基础上 / 2。(对于正数、负数都适用)

  1. 面试题:高效的方式计算2 * 8 ? (2 << 3 或 8 << 1)

条件运算符

  1. (条件表达式)? 表达式1 : 表达式2

  2. 说明:
    ① 条件表达式的结果是boolean类型。
    ② 如果条件表达式的结果是true,则执行表达式1。否则,执行表达式2。
    ③ 表达式1 和 表达式2 需要是相同的类型或能兼容的类型。

④ 开发中,凡是可以使用条件运算符的位置,都可以改写为if-else。
反之,能使用if-else结构,不一定能改写为条件运算符。

建议,在二者都能使用的情况下,推荐使用条件运算符。因为执行效率稍高。

企业真题(二)

1. 高效的方式计算2 * 8的值 (文**辉、轮*科技)

使用 <<

2. &和&&的区别?(恒*电子、*度)

1、相同点:两个符号表达的都是”且”的关系。只有当符号左右两边的类型值均为true时,结果才为true。
2、执行过程:
1)如果符号左边是true,则& 、&& 都会执行符号右边的操作
2)如果符号左边是false,则 & 会继续执行符号右边的操作,&& 不会执行符号右边的操作

3. Java中的基本类型有哪些?String 是最基本的数据类型吗?(恒*电子)

8种基本数据类型。(略)

String不是,属于引用数据类型。

4. Java中的基本数据类型包括哪些?(*米)

1
2
类似问题:
> Java的基础数据类型有哪些?String是吗?(贝壳)

5. Java开发中计算金额时使用什么数据类型?(5*到家)

不能使用float或double,因为精度不高。

使用BigDecimal类替换,可以实现任意精度的数据的运算。

6. char型变量中能不能存储一个中文汉字,为什么?(*通快递)

可以的。char c1 = ‘中’;

char c2 = ‘a’。

因为char使用的是unicode字符集,包含了世界范围的所有的字符。

7. 代码分析(君*科技、新*陆)

1
2
short s1=1; 
s1=s1+1; //有什么错? =右边是int类型。需要强转
1
2
short s1=1;
s1+=1; //有什么错? 没错

8. int i=0; i=i++执行这两句化后变量 i 的值为(*软)

0。

9. 如何将两个变量的值互换(北京*彩、中外*译咨询)

1
2
3
4
5
6
String s1 = "abc";
String s2 = "123";

String temp = s1;
s1 = s2;
s2 = temp;

10. boolean 占几个字节(阿**巴)

1
2
3
4
5
6
7
编译时不谈占几个字节。

但是JVM在给boolean类型分配内存空间时,boolean类型的变量占据一个槽位(slot,等于4个字节)。
细节:true:1 false:0

>拓展:在内存中,byte\short\char\boolean\int\float : 占用1个slot
double\long :占用2个slot

11. 为什么Java中0.1 + 0.2结果不是0.3?(字*跳动)

在代码中测试0.1 + 0.2,你会惊讶的发现,结果不是0.3,而是0.3000……4。这是为什么?

几乎所有现代的编程语言都会遇到上述问题,包括 JavaScript、Ruby、Python、Swift 和 Go 等。引发这个问题的原因是,它们都采用了IEEE 754标准

IEEE是指“电气与电子工程师协会”,其在1985年发布了一个IEEE 754计算标准,根据这个标准,小数的二进制表达能够有最大的精度上限提升。但无论如何,物理边界是突破不了的,它仍然不能实现“每一个十进制小数,都对应一个二进制小数”。正因如此,产生了0.1 + 0.2不等于0.3的问题。

具体的:

整数变为二进制,能够做到“每个十进制整数都有对应的二进制数”,比如数字3,二进制就是11;再比如,数字43就是二进制101011,这个毫无争议。

对于小数,并不能做到“每个小数都有对应的二进制数字”。举例来说,二进制小数0.0001表示十进制数0.0625 (至于它是如何计算的,不用深究);二进制小数0.0010表示十进制数0.125;二进制小数0.0011表示十进制数0.1875。看,对于四位的二进制小数,二进制小数虽然是连贯的,但是十进制小数却不是连贯的。比如,你无法用四位二进制小数的形式表示0.125 ~ 0.1875之间的十进制小数。

所以在编程中,遇见小数判断相等情况,比如开发银行、交易等系统,可以采用四舍五入或者“同乘同除”等方式进行验证,避免上述问题。

分支结构

if-else

1
2
3
4
5
if(条件表达式) { 
语句块1;
}else{
语句块2;
}

switch-case

  • 在特殊的场景下,分支结构可以考虑使用switch-case

    • 指定的数据类型:byte \ short \ char \ int ; 枚举类(jdk5.0)\ String (jdk7.0)
    • 可以考虑的常量值有限且取值情况不多。
  • 特别之处:case穿透。

  • 在能使用switch-case的情况下,推荐使用switch-case,因为比if-else效率稍高

1
2
3
4
5
6
7
8
9
10
11
12
13
switch(表达式){

case 常量1:
//执行语句1
//break;
case 常量2:
//执行语句2
//break;
...
default:
//执行语句2
//break;
}

循环结构

for

  • 凡是循环结构,都有4个要素:①初始化条件 ②循环条件(是boolean类型) ③ 循环体 ④ 迭代条件
  • 应用场景:有明确的遍历的次数。 for(int i = 1;i <= 100;i++)

while

  • 应用场景:没有明确的遍历次数。

do-while

  • 至少会执行一次循环体。
  • 开发中,使用的较少

break和continue

  • break在开发中常用;而continue较少使用
  • 笔试题:break和continue的区别。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /*
    1. break和continue关键字的使用

    使用范围 在循环结构中的作用 相同点
    break: switch-case
    循环结构中 结束(或跳出)当前循环结构 在此关键字的后面不能声明执行语句。

    continue: 循环结构中 结束(或跳出)当次循环 在此关键字的后面不能声明执行语句。


    2. 了解带标签的break和continue的使用

    3. 开发中,break的使用频率要远高于continue。
    */

Math类的random()

  1. random()调用以后,会返回一个[0.0,1.0)范围的double型的随机数
  2. 需求:获取一个[a,b]范围的随机整数?
    (int)(Math.random() * (b - a + 1)) + a

企业真题(三)

1. break和continue的作用(智*图)

2. if分支语句和switch分支语句的异同之处(智*图)

  • if-else语句优势
    • if语句的条件是一个布尔类型值,if条件表达式为true则进入分支,可以用于范围的判断,也可以用于等值的判断,使用范围更广
    • switch语句的条件是一个常量值(byte,short,int,char,枚举,String),只能判断某个变量或表达式的结果是否等于某个常量值,使用场景较狭窄
  • switch语句优势
    • 当条件是判断某个变量或表达式是否等于某个固定的常量值时,使用if和switch都可以,习惯上使用switch更多。因为效率稍高。当条件是区间范围的判断时,只能使用if语句。
    • 使用switch可以利用穿透性,同时执行多个分支,而if…else没有穿透性。

3. 什么时候用语句if,什么时候选用语句switch(灵伴*来科技)

同上

4. switch语句中忘写break会发生什么(北京*蓝)

case穿透

5. Java支持哪些类型循环(上海*睿)

  • for;while;do-while
  • 增强for (或foreach),放到集合中讲解

6. while和do while循环的区别(国*科技研究院)

  • do-while至少会执行一次。

IDEA的认识

  • IDEA(集成功能强大、符合人体工程学(设置人性化))
  • Eclipse

企业真题(四)

1. 开发中你接触过的开发工具都有哪些?

IDEA

2. 谈谈你对Eclipse和IDEA使用上的感受?

Eclipse不够人性化。

数组

数组的概述

  • 数组,就可以理解为多个相同数据的组合。
  • 是程序中的容器:数组、集合框架(List、Set、Map)
  • 数组存储的数据的特点:依次紧密排列的、有序的、可以重复的
  • 此时的数组、集合框架都是在内存中对多个数据的存储。
  • 数组的其它特点:一旦初始化,其长度就是确定的、不可更改的。
  • 数组名中引用的是这块连续空间的首地址。

一维数组的使用(重要)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> 数组的声明和初始化
int[] arr = new int[10];
String[] arr1 = new String[]{"Tom","Jerry"};
注意:Java 语言中声明数组时不能指定其长度(数组中元素的个数)。 例如:int a[5]; //非法
> 调用数组的指定元素:使用角标、索引、index
>index从0开始!因为第一个元素距离数组首地址间隔 0 个单元格。
> 数组的属性:length,表示数组的长度
> 数组的遍历
> 数组元素的默认初始化值
对于基本数据类型而言,默认初始化值各有不同。
对于引用数据类型而言,默认初始化值为 null(注意与 0 不同!)
> 一维数组的内存解析(难)
前提:在main()中声明变量:int[] arr = new int[]{1,2,3};
> 虚拟机栈:main()作为一个栈帧,压入栈空间中。在main()栈帧中,存储着arr变量。arr记录着数组实体的首地址值。
> 堆:数组实体存储在堆空间中。

二维数组的使用(难点)

  • 二维数组:一维数组的元素,又是一个一维数组,则构成了二维数组。
1
2
3
4
5
6
> 数组的声明和初始化
> 调用数组的指定元素
> 数组的属性:length,表示数组的长度
> 数组的遍历
> 数组元素的默认初始化值
> 二维数组的内存解析(难)

数组的常用算法(重要)

  • 数值型数组的特征值的计算:最大值、最小值、总和、平均值等
  • 数组元素的赋值。比如:杨辉三角;彩票随机生成数(6位;1-30;不能重复);回形数
  • 数组的复制、赋值
  • 数组的反转
  • 数组的扩容、缩容
  • 数组的查找
    • 线性查找
    • 二分法查找(前提:数组有序)
  • 数组的排序
    • 冒泡排序:最简单
    • 快速排序:最常用

Arrays工具类的使用

  • 熟悉一下内部的常用的方法
    • toString() / sort() / binarySearch()

数组中的常见异常

  • ArrayIndexOutOfBoundsException
  • NullPointerException

企业真题(五)

1. 数组有没有length()这个方法? String有没有length()这个方法?(*蓝)

数组没有length(),是length属性。

String有length()

2. 有数组int[] arr,用Java代码将数组元素顺序颠倒(闪*购)

3. 为什么数组要从0开始编号,而不是1(中*支付)

数组的索引,表示了数组元素距离首地址的偏离量。因为第1个元素的地址与首地址相同,所以偏移量就是0。所以从0开始。

4. 数组有什么排序的方式,手写一下(平*保险)

冒泡。

快排。(讲完递归方法以后,大家就可以练习一下)

5. 常见排序算法,说下快排过程,时间复杂度?(5*到家)

见课件。

快排:O(nlogn)

6. 二分算法实现数组的查找(神舟*天软件)

7. 怎么求数组的最大子序列和(携*)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
* 输入一个整形数组,数组里有正数也有负数。数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和。
* 求所有子数组的和的最大值。要求时间复杂度为O(n)。
例如:输入的数组为1, -2, 3, 10, -4, 7, 2, -5,和最大的子数组为3, 10, -4, 7, 2,
因此输出为该子数组的和18。
*/
public class ArrDemo {
public static void main(String[] args) {
int[] arr = new int[]{1, -2, 3, 10, -4, 7, 2, -5};
int i = getGreatestSum(arr);
System.out.println(i);
}

public static int getGreatestSum(int[] arr){
int greatestSum = 0;
if(arr == null || arr.length == 0){
return 0;
}
int temp = greatestSum;
for(int i = 0;i < arr.length;i++){
temp += arr[i];

if(temp < 0){
temp = 0;
}

if(temp > greatestSum){
greatestSum = temp;
}
}
if(greatestSum == 0){
greatestSum = arr[0];
for(int i = 1;i < arr.length;i++){
if(greatestSum < arr[i]){
greatestSum = arr[i];
}
}
}
return greatestSum;
}
}

8. Arrays 类的排序方法是什么?如何实现排序的?(阿*、阿*校招)

面向对象-基础

面向过程 vs 面向对象(了解)

  • 不管是面向过程、面向对象,都是程序设计的思路。
  • 面向过程:以函数为基本单位,适合解决简单问题。比如:开车
  • 面向对象:以类为基本单位,适合解决复杂问题。比如:造车

类、对象

  • 类:抽象的,概念上的定义
  • 对象:具体的,类的一个一个的实例。
  • 面向对象完成具体功能的操作的三步流程(非常重要)
1
2
3
步骤1:创建类,并设计类的内部成员(属性、方法)
步骤2:创建类的对象。比如:Phone p1 = new Phone();
步骤3:通过对象,调用其内部声明的属性或方法,完成相关的功能
  • 对象的内存解析
    • 创建类的一个对象;创建类的多个对象;方法的调用的内存解析
  • Java中内存结构的划分
    • Java中内存结构划分为:虚拟机栈、堆、方法区;程序计数器、本地方法栈
    • 虚拟机栈:以栈帧为基本单位,有入栈和出栈操作;每个栈帧入栈操作对应一个方法的执行;方法内的局部变量会存储在栈帧中。
    • 堆空间:new 出来的结构(数组、对象):① 数组,数组的元素在堆中 ② 对象的成员变量在堆中。
    • 方法区:加载的类的模板结构。

类的成员之一:属性(或成员变量)

  • 成员变量 vs 局部变量
    相同点:
  • 变量声明的格式相同: 数据类型 变量名 = 初始化值
  • 变量必须先声明、后初始化、再使用。
  • 变量都有其对应的作用域。只在其作用域内是有效的
    不同点:
  1. 声明位置和方式 (1)实例变量:在类中方法外 (2)局部变量:在方法体
    {}中或方法的形参列表、代码块中
  2. 在内存中存储的位置不同 (1)实例变量:堆 (2)局部变量:栈
  3. 生命周期 (1)实例变量:和对象的生命周期一样,随着对象的创建而存
    在,随着对象被 GC 回收而消亡, 而且每一个对象的实例变量是独立的。
    (2)局部变量:和方法调用的生命周期一样,每一次方法被调用而在存在,随
    着方法执行的结束而消亡, 而且每一次方法调用都是独立。
  4. 作用域 (1)实例变量:通过对象就可以使用,本类中直接调用,其他类中
    “对象.实例变量” (2)局部变量:出了作用域就不能使用
  5. 修饰符(后面来讲) (1)实例变量:
    public,protected,private,final,volatile,transient 等 (2)局部变量:final
  6. 默认值 (1)实例变量:有默认值 (2)局部变量:没有,必须手动初始
    化。其中的形参比较特殊,靠实参给它初始化。
  • 属性 <=> 成员变量 <=>field <=> 字段、域

类的成员之二:方法

  • 方法的声明:权限修饰符 返回值类型 方法名(形参列表){ // 方法体}
    • 重点:返回值类型、形参列表
  • return关键字的使用

再谈方法

方法的重载(overload)

  • 方法的重载的要求:“两同一不同” 同类同名参数不同
  • 调用方法时,如何确定调用的是某个指定的方法呢?① 方法名 ② 形参列表

可变个数形参的方法

JDK 5.0 中提供了 Varargs(variable number of arguments)机制

  • 格式:(int … args)

方法的参数传递机制:值传递(重点、难点)

1
2
> 如果形参是基本数据类型的变量,则将实参保存的数据值赋给形参。
> 如果形参是引用数据类型的变量,则将实参保存的地址值赋给形参。

递归方法

  • 递归方法构成了隐式的循环
  • 对比:相较于循环结构,递归方法效率稍低,内存占用偏高。
  • 递归调用会占用大量的系统堆栈,内存耗用多,在递归调用层次多时速度要比循环慢的多,所以在使用递归时要慎重。
  • 在要求高性能的情况下尽量避免使用递归,递归调用既花时间又耗内存。考虑使用循环迭代

对象数组

  • String[] ;Person[] ; Customer[]

package、import关键字的使用

  • package:指明声明的类所属的包。
  • import:当前类中,如果使用其它包下的类(除java.lang包),原则上就需要导入。
  1. java.lang—-包含一些 Java 语言的核心类,如 String、Math、Integer、System 和 Thread,提供常用功能
  2. java.net—-包含执行与网络相关的操作的
    类和接口。
  3. java.io —-包含能提供多种输入/输出功能的类。
  4. java.util—-包含一些实用工具类,如定义系统特性、接口的集合框架类、使用与日期日历相关的函数。
  5. java.text—-包含了一些 java 格式化相关的类
  6. java.sql—-包含了 java 进行 JDBC 数据库编程的相关类/接口
  7. java.awt—-包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)

oop的特征之一:封装性

1
2
Java规定了4种权限修饰,分别是:private、缺省、protected、public。
我们可以使用4种权限修饰来修饰类及类的内部成员。当这些成员被调用时,体现可见性的大小。

举例:

1
2
3
> 场景1:私有化(private)类的属性,提供公共(public)的get和set方法,对此属性进行获取或修改
> 场景2:将类中不需要对外暴露的方法,设置为private
> 场景3:单例模式中构造器private的了,避免在类的外部创建实例。(放到static关键字后讲)

上理论:程序设计的原则之一

1
2
3
4
5
理论上:
-`高内聚`:类的内部数据操作细节自己完成,不允许外部干涉;
(Java程序通常以类的形态呈现,相关的功能封装到方法中。)
-`低耦合`:仅暴露少量的方法给外部使用,尽量方便外部调用。
(给相关的类、方法设置权限,把该隐藏的隐藏起来,该暴露的暴露出去)

注意:
开发中,一般成员实例变量都习惯使用 private 修饰,再提供相应的public 权限的 get/set 方法访问。
对于 final 的实例变量,不提供 set()方法。
对于 static final 的成员变量,习惯上使用 public 修饰。

类的成员之三:构造器

  • 如何定义:权限修饰符 类名(形参列表){}
  • 构造器的作用:① 搭配上new,用来创建对象 ② 初始化对象的成员变量

三个小知识

类的实例变量的赋值过程(重要)

  1. 在类的属性中,可以有哪些位置给属性赋值?
    ① 默认初始化;
    ② 显式初始化;
    ③ 构造器中初始化;

④ 通过”对象.方法”的方式赋值;
⑤ 通过”对象.属性”的方式赋值;

  1. 这些位置执行的先后顺序是怎样?
    ① - ② - ③ - ④/⑤

JavaBean

所谓JavaBean,是指符合如下标准的Java类:

  • 类是公共的
  • 有一个无参的公共的构造器
  • 有属性,且有对应的get、set方法

UML类图

熟悉。

企业真题(六)

2.1 类与对象

1. 面向对象,面向过程的理解?(平*金服、英**达)

略。

2. Java 的引用类型有哪几种(阿*校招)

类、数组、接口;枚举、注解、记录

3. 类和对象的区别(凡*科技、上*银行)

略。

4. 面向对象,你解释一下,项目中哪些地方用到面向对象?(燕*金融)

“万事万物皆对象”。

2.2 Java内存结构

1. Java虚拟机中内存划分为哪些区域,详细介绍一下(神**岳、数*互融)

略。

2. 对象存在Java内存的哪块区域里面?(阿*)

堆空间。

2.3 权限修饰符(封装性)

1. private 、缺省、protected、public的表格化作用区域(爱*信、拓*思、中*瑞飞)

2. main方法的public能不能换成private?为什么?(凡*科技、顺*)

能。但是改以后就不能作为程序的入口了,就只是一个普通的方法。

2.4 构造器

1. 构造方法和普通方法的区别(凡*科技、软*动力、中*软)

编写代码的角度:没有共同点。声明格式、作用都不同。

字节码文件的角度:构造器会以<init>()方法的形态呈现,用以初始化对象。

2. 构造器Constructor是否可被overload?(鸿*网络)

可以。

3. 无参构造器和有参构造器的的作用和应用(北京楚*龙)

2.5 属性及属性赋值顺序

1. 成员变量与局部变量的区别(艾*软件)

6个点。

2. 变量赋值和构造方法加载的优先级问题(凡*科技、博*软件)

变量显式赋值先于构造器中的赋值。

如何证明?我看的字节码文件。

面向对象-进阶

this关键字的使用

  • this调用的结构:属性、方法;构造器

  • this调用属性或方法时,理解为:当前对象或当前正在创建的对象。

    1
    2
    3
    4
    5
    6
    7
    8
    public void setName(String name){ //当属性名和形参名同名时,必须使用this来区分
    this.name = name;
    }

    public Person(String name){
    this();
    this.name = name;
    }
  • this():调用本类的无参构造器。

  • this(形参列表)的方式,表示调用当前类中其他的重载的构造器。
    注意:
    • 不能出现递归调用。比如,调用自身构造器。
    – 推论:如果一个类中声明了 n 个构造器,则最多有 n - 1 个构造器中使用了”this(形参列表)”
    • this()和 this(实参列表)只能声明在构造器首行。
    – 推论:在类的一个构造器中,最多只能声明一个”this(参数列表)”

面向对象的特征二:继承性

  • 继承性的好处
    • 继承的出现减少了代码冗余,提高了代码的复用性。
    • 继承的出现,更有利于功能的扩展。
    • 继承的出现让类与类之间产生了 is-a 的关系,为多态的使用提供了前提。继承描述事物之间的所属关系,这种关系是:is-a 的关系。可见,父类更通用、更一般,子类更具体。
    • 注意:不要仅为了获取其他类中某个功能而去继承
  • Java中继承性的特点
    • 局限性:类的单继承性。后续我们通过类实现接口的方式,解决单继承的局限性。
    • 支持多层继承,一个父类可以声明多个子类。
  • 基础:class A extends B{}
    类 A,称为子类、派生类(derived class)、SubClass
    类 B,称为父类、超类、基类(base class)、SuperClass
  • 理解:子类就获取了父类中声明的全部的属性、方法。可能受封装性的影响,不能直接调用。

方法的重写(override / overwrite)

@Override 使用说明:
写在方法上面,用来检测是不是满足重写方法的要求。这个注解就算
不写,只要满足要求,也是正确的方法覆盖重写。建议保留,这样编
译器可以帮助我们检查格式,另外也可以让阅读源代码的程序员清晰
的知道这是一个重写的方法。

  • 要求
  1. 子类重写的方法必须和父类被重写的方法具有相同的方法名称、参数列表。
  2. 子类重写的方法的返回值类型不能大于父类被重写的方法的返回值类型。(例如:
    Student < Person)。注意:如果返回值类型是基本数据类型和 void,那么必须是相同。
  3. 子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限。(public > protected > 缺省 > private)注意:① 父类私有方法不能重写 ② 跨包的父类缺省的方法也不能重写
  4. 子类方法抛出的异常不能大于父类被重写方法的异常
  5. 此外,子类与父类中同名同参数的方法必须同时声明为非 static 的(即为重写),
    或者同时声明为 static 的(不是重写)。因为 static 方法是属于类的,子类无法
    覆盖父类的方法。
  • 面试题:方法的重载与重写的区别?
    • 方法的重载:“两同一不同”
    • 方法的重写:
      • 前提:类的继承关系
      • 子类对父类中同名同参数方法的覆盖、覆写。

super关键字的使用

  • super可以调用的结构:属性、方法;构造器
  • super:父类的
  • super调用父类的属性、方法:
    • 如果子父类中出现了同名的属性,此时使用super.的方式,表明调用的是父类中声明的属性。
    • 子类重写了父类的方法。如果子类的任何一个方法中需要调用父类被重写的方法时,需要使用super.
  • super调用构造器:
    • 子类继承父类时,不会继承父类的构造器。只能通过“super(形参列表)”的方
      式调用父类指定的构造器。
    • 在子类的构造器中,首行要么使用了”this(形参列表)”,要么使用了”super(形参列表)”。
    • 如果在子类构造器的首行既没有显示调用”this(形参列表)”,也没有显式调用
      “super(形参列表)”, 则子类此构造器默认调用”super()”,即调用父类中空参的
      构造器。
    • 一个类中声明有 n 个构造器,最多有 n-1 个构造器中使用了”this(形参列表)”,则剩下的那个一定使用”super(形参列表)”

(熟悉)子类对象实例化的全过程

  • 结果上来说:体现为继承性
  • 过程上来说:子类调用构造器创建对象时,一定会直接或间接的调用其父类的构造器,以及父类的父类的构造器,…,直到调用到Object()的构造器。

面向对象的特征三:多态性

  • 广义上的理解:子类对象的多态性、方法的重写;方法的重载

    狭义上的理解:子类对象的多态性。

  • 格式:Object obj = new String(“hello”); Person p = new Man();
    父类的引用指向子类的对象。

  • 多态的好处:减少了大量的重载的方法的定义;开闭原则

    • 举例:public boolean equals(Object obj)
    • 多态,无处不在!讲了抽象类、接口以后,会有更好的理解。
  • 弊端:一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法。

  • 多态的使用:虚拟方法调用。“编译看左边,运行看右边”。属性不存在多态性。

  • 多态的逆过程:向下转型,使用强转符()。

    • 为了避免出现强转时的ClassCastException,建议()之前使用instanceOf进行判断。
  • 向下转型:(子类类型)父类变量

    • Person p2 = new Man(); Man m1 = (Man)p2
    • 建议在向下转型之前,使用instanceof进行判断,避免出现类型转换异常
    • 格式:a instanceof A :判断对象a是否是类A的实例

Object类的使用

  • 根父类,默认的父类,java.lang.Object

  • equals()

    • 只能比较引用类型,Object 类源码中 equals()的作用与“==”相同:比较是否指向同一个对象。对类 File、String、Date 及包装类(Wrapper Class)来说,是比较类型及内容而不考虑引用的是否是同一个对象;在这些类中重写了 Object 类的 equals()方法。
    • 重写和不重写的区别
    • 面试题: == 和 equals()
      1
      2
      3
      4
      1. == 既可以比较基本类型也可以比较引用类型。对于基本类型就是比较值,对于引用类型就是比较内存地址
      2. equals 的话,它是属于 java.lang.Object 类里面的方法,如果该方法没有被重写过默认也是==;我们可以看到 String 等类的 equals 方法是被重写过的,而且 String 类在日常开发中用的比较多,久而久之,形成了 equals 是比较值的错误观点。
      3. 具体要看自定义类里有没有重写 Object 的 equals 方法来判断。
      通常情况下,重写 equals 方法,会比较类中的相应属性是否都相等。
  • toString()的使用

    • Object中toString()调用后,返回当前对象所属的类和地址值。
    • 开发中常常重写toString(),用于返回当前对象的属性信息。
  • clone() 深拷贝

  • finalize() 当GC要回收此对象时,调用该方法(从JDK9开始过时),可能导致内部出现循环引用,导致此对象不能被回收

  • getClass() \ hashCode() \ notify() \ notifyAll() \ wait()

企业真题(七)

2.1 继承性

1. 父类哪些成员可以被继承,属性可以被继承吗?可以或者不可以,请举下例子。(北京明**信)

父类的属性、方法可以被继承。构造器可以被子类调用。

2.2 重写

1. 什么是Override,与Overload的区别(顺*、软**力、明*数据、阳*科技、中*软)

2. Overload的方法是否可以改变返回值的类型?(新*陆)

和返回值类型无关。

public void method(int i){}

public int method(int j,int k){}

3. 构造器Constructor是否可被override?(鸿*网络、深圳德**技、航**普)

不能!构造器可以重载

4. 为什么要有重载,我随便命名一个别的函数名不行吗?谈谈你是怎么理解的。(腾*)

见名知意。

2.3 super关键字

1. super和this的区别(蚂**服)

把两个关键字各自的特点说清楚。

2. this、super关键字分别代表什么?以及他们各自的使用场景和作用。(北京楚*龙)

2.4 多态

1. 谈谈你对多态的理解(三*重工、江*智能、银*数据、君*科技)

1
2
3
4
类似问法:
> Java中实现多态的机制是什么(国*电网)
> 什么是多态?(上*银行)
> Java中的多态是什么意思?(贝*)

理解、格式、好处、弊端。

2. 多态new出来的对象跟不多态new出来的对象区别在哪?(万*智能)

Person p = new Man(); //虚方法调用。屏蔽了子类Man类特有的属性和方法。

Man m = new Man();

3. 说说你认为多态在代码中的体现(楚*龙)

无处不在!

2.5 Object类

1. ==与equals的区别(拓*思)

1
2
类似问法:
> 两个对象A和B,A==B,A.equals(B)有什么区别(华油**普)

2. 重写equals方法要注意什么?(安**网络科技)

  • 明确判定两个对象实体equals()的标准。是否需要所有的属性参与。
  • 对象的属性,又是自定义的类型,此属性也需要重写equals()

3. Java中所有类的父类是什么?他都有什么方法?(阿*校招)

1
2
相关问题:
> Object类有哪些方法?(恒*电子)

面向对象-高级

关键字:static

  • static:静态的,随着类的加载而加载、执行。

  • static用来修饰:属性、方法、代码块、内部类

    1
    2
    3
    4
    5
    6
    7
    [修饰符] class 类{
    [其他修饰符] static 数据类型 变量名;

    [其他修饰符] static 返回值类型 方法名(形参列表){
    方法体
    }
    }
  • 被修饰后的成员具备以下特点:

    1. 随着类的加载而加载
    2. 优先于对象存在
    3. 修饰的成员,被所有对象所共享
    4. 访问权限允许时,可不创建对象,直接被类调用
  • 熟悉:static修饰的类变量、类方法与不使用static修饰的区别。

    • 类变量:类的生命周期内,只有一个。被类的多个实例共享。
  • 掌握:我们遇到属性或方法时,需要考虑是否声明为static的。

单例模式(或单子模式)

  • 经典的设计模式有23种
  • 解决的问题:在整个软件系统中,只存在当前类的唯一实例。
  • 实现方式:饿汉式、懒汉式、枚举类等
  • 对比饿汉式和懒汉式
    • 饿汉式:“立即加载”,线程安全的。
    • 懒汉式:”延迟加载”,线程不安全。
  • 需要会手写饿汉式和懒汉式
    • 饿汉式:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      class Singleton {
      // 1.私有化构造器
      private Singleton() {
      }
      // 2.内部提供一个当前类的实例
      // 4.此实例也必须静态化
      private static Singleton single = new Singleton();
      // 3.提供公共的静态的方法,返回当前类的对象
      public static Singleton getInstance() {
      return single;
      }
      }
    • 懒汉式:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      class Singleton {
      // 1.私有化构造器
      private Singleton() {
      }
      // 2.内部提供一个当前类的实例
      // 4.此实例也必须静态化
      private static Singleton single;
      // 3.提供公共的静态的方法,返回当前类的对象
      public static Singleton getInstance() {
      if(single == null) {
      single = new Singleton();
      }
      return single;
      }
      }
  • 优点和应用场景
    由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比
    较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时
    直接产生一个单例对象,然后永久驻留内存的方式来解决。

理解main()方法

由于 JVM 需要调用类的 main()方法,所以该方法的访问权限必须是 public,又
因为 JVM 在执行 main()方法时不必创建对象,所以该方法必须是 static 的,该
方法接收一个 String 类型的数组参数,该数组中保存执行 Java 命令时传递给所
运行的类的参数。
又因为 main() 方法是静态的,我们不能直接访问该类中的非静态成员,必须创
建该类的一个实例对象后,才能通过这个对象去访问类中的非静态成员,这种
情况,我们在之前的例子中多次碰到。

  • public static void main(String[] args){}
  • 理解1:作为程序的入口;普通的静态方法
  • 理解2:如何使用main()与控制台进行数据的交互。
    • 命令行:java 类名 “Tom” “Jerry” “123”

类的成员之四:代码块

1
2
3
4
5
[修饰符] class 类{
static{
静态代码块
}
}
  • 分类:静态代码块、非静态代码块
  • 使用频率上来讲:用的比较少。
  • 静态代码块:
    1. 可以有输出语句。
    2. 可以对类的属性、类的声明进行初始化操作。
    3. 不可以对非静态的属性初始化。即:不可以调用非静态的属性和方法。
    4. 若有多个静态的代码块,那么按照从上到下的顺序依次执行。
    5. 静态代码块的执行要先于非静态代码块。
    6. 静态代码块随着类的加载而加载,且只执行一次。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      private static String country;
      private String name;
      {
      System.out.println("非静态代码块,country = " + country);
      }
      static {
      country = "中国";
      System.out.println("静态代码块");
      }
  • 非静态代码块:随着对象的创建而执行

总结:对象的实例变量可以赋值的位置及先后顺序

① 默认初始化
② 显式初始化 或 ⑤ 代码块中初始化
③ 构造器中初始化

④ 有了对象以后,通过”对象.属性”或”对象.方法”的方法进行赋值

执行的先后顺序:
① - ②/⑤ - ③ - ④

关键字:final

  • 用来修饰:类、方法、变量(成员变量、局部变量)
    • 类:不能被继承,没有子类。提高安全性,提高程序的可读性。
    • 方法:不能被子类重写。
    • 变量:是一个“常量”,一旦赋值不能修改。即常量,常量名建议使用大写字母。如果某个成员变量用 final 修饰后,没有 set 方法,并且必须初始化(可以显式赋值、或在初始化块赋值、实例变量还可以在构造器中赋值)

关键字:abstract

  • 抽象的
  • 用来修饰:类、方法
    • 类:抽象类:不能实例化。
    • 方法:抽象方法:没有方法体,必须由子类实现此方法。
  • 使用说明:
    1. 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对
      象。
      理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方
      法体,没有意义。
    2. 抽象类是用来被继承的,抽象类的子类必须重写父类的抽象方法,并提供方法
      体。若没有重写全部的抽象方法,仍为抽象类。
    3. 抽象类中,也有构造方法,是供子类创建对象时,初始化父类成员变量使用的。
      理解:子类的构造方法中,有默认的 super()或手动的 super(实参列表),需要
      访问父类构造方法。
    4. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
      理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常
      用于某些特殊的类结构设计。
    5. 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。
      除非该子类也是抽象类。
      理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象
      后,调用抽象的方法,没有意义。
  • 注意事项:
    • 不能用 abstract 修饰变量、代码块、构造器;
    • 不能用 abstract 修饰私有方法、静态方法、final 的方法、final 的类。

关键字:interface

  • interface:接口,用来定义一组规范、一种标准。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    [修饰符] interface 接口名{
    //接口的成员列表:
    // 公共的静态常量
    // 公共的抽象方法

    // 公共的默认方法(JDK1.8 以上)
    // 公共的静态方法(JDK1.8 以上)
    // 私有方法(JDK1.9 以上)
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public interface USB3{
    //静态常量
    long MAX_SPEED = 500*1024*1024;//500MB/s
    //抽象方法
    void in();
    void out();
    //默认方法
    default void start(){
    System.out.println("开始");
    }
    default void stop(){
    System.out.println("结束");
    }
    //静态方法
    static void show(){
    System.out.println("USB 3.0 可以同步全速地进行读写操作");
    }
    }
  • 掌握:接口中可以声明的结构。

    • 属性:使用public static final修饰

    • 方法:jdk8之前:只能声明抽象方法,使用public abstract修饰

      ​ jdk8中:声明static方法、default方法。

      ​ jdk9中:声明private方法。

  • 类实现接口(implements):接口不能创建对象,但是可以被类实现(implements ,类似于被继承)。类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements 关键字。

  • 接口的多实现(implements):在继承体系中,一个类只能继承一个父类。而对于接口而言,一个类是可以实现多个接口的,这叫做接口的多实现。并且,一个类能继承一个父类,同时实现多个接口。

  • 接口的多继承(extends):一个接口能继承另一个或者多个接口。

  • JDK8 中相关冲突问题:

  1. 默认方法冲突问题:
    (1)类优先原则:当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的抽象方法重名,子类就近选择执行父类的成员方法。
    (2)接口冲突(左右为难):当一个类同时实现了多个父接口,而多个父接口中包含方法签名相同的默认方法时,选择保留其中一个,通过“接口名.super.方法名”的方法选择保留哪个接口的默认方法;当一个子接口同时继承了多个接口,而多个父接口中包含方法签名相同的默认方法
    时,子接口重写默认方法。(子接口重写默认方法时,default 关键字可以保留;子类重写默认方法时,default 关键字不可以保留)
  2. 常量冲突问题:
    当子类继承父类又实现父接口,而父类中存在与父接口常量同名的成员变量,并且该成员变量名在子类中仍然可见。
    当子类同时实现多个接口,而多个接口存在相同同名常量。
    此时在子类中想要引用父类或父接口的同名的常量或成员变量时,就会有冲突问题。
  • 笔试题:抽象类、接口的对比。

类的成员之五:内部类

1
2
3
4
> 成员内部类的理解
> 如何创建成员内部类的实例
> 如何在成员内部类中调用外部类的结构
> 局部内部类的基本使用(关注:如何在方法内创建匿名局部内部类的对象)

枚举类:enum

  • 枚举类的实现:
    – 在 JDK5.0 之前,需要程序员自定义枚举类型。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Season{
    private final String SEASONNAME;//季节的名称
    private final String SEASONDESC;//季节的描述
    private Season(String seasonName,String seasonDesc){
    this.SEASONNAME = seasonName;
    this.SEASONDESC = seasonDesc;
    }
    public static final Season SPRING = new Season("春天", "春暖花开");
    public static final Season SUMMER = new Season("夏天", "夏日炎炎");
    public static final Season AUTUMN = new Season("秋天", "秋高气爽");
    public static final Season WINTER = new Season("冬天", "白雪皑皑");
    @Override
    public String toString() {
    return "Season{" +
    "SEASONNAME='" + SEASONNAME + '\'' +
    ", SEASONDESC='" + SEASONDESC + '\'' +
    '}';
    }
    }
    – 在 JDK5.0 之后,Java 支持 enum 关键字来快速定义枚举类型。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public enum SeasonEnum {
    SPRING("春天","春风又绿江南岸"),
    SUMMER("夏天","映日荷花别样红"),
    AUTUMN("秋天","秋水共长天一色"),
    WINTER("冬天","窗含西岭千秋雪");
    private final String seasonName;
    private final String seasonDesc;

    private SeasonEnum(String seasonName, String seasonDesc) {
    this.seasonName = seasonName;
    this.seasonDesc = seasonDesc;
    }
    public String getSeasonName() {
    return seasonName;
    }
    public String getSeasonDesc() {
    return seasonDesc;
    }
    }
  • enum 中常用方法:
    String toString(): 默认返回的是常量名(对象名),可以继续手动重写该方法!

static 枚举类型[] values():返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值,是一个静态方法

static 枚举类型 valueOf(String name):可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常:IllegalArgumentException。

String name():得到当前枚举常量的名称。建议优先使用 toString()。

int ordinal():返回当前枚举常量的次序号,默认从 0 开始

  • 实现接口的枚举类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    enum A implements 接口 1,接口 2{
    常量名 1(参数){
    //抽象方法的实现或重写
    },
    常量名 2(参数){
    //抽象方法的实现或重写
    },
    //...
    }
  • 掌握:使用enum关键字定义枚举类即可。

注解:Annotation

  • 框架 = 注解 + 反射 + 设计模式
  • Java基础阶段:简单。@Override(限定重写父类方法,该注解只能用于方法) 、 @Deprecated(用于表示所修饰的元素(类,方法等)已过时)、@SuppressWarnings(抑制编译器警告)
  • 元注解:对现有的注解进行解释说明。
    • @Target:表明可以用来修饰的结构
    • @Retation:表明生命周期
  • 如何自定义注解。
    1
    2
    3
    4
    [元注解]
    [修饰符] @interface 注解名{
    [成员列表]
    }
  • JUnit 单元测试
    默认情况下,在单元测试方法中使用 Scanner 时,并不能实现控制台数据的输
    入。需要做如下设置:
    在 idea64.exe.vmoptions 配置文件中加入下面一行设置,重启 idea 后生效。
    -Deditable.java.test.console=true

包装类的使用

  • 掌握:基本数据类型对应的包装类都有哪些?
  • 掌握:基本数据类型、包装类、String三者之间的转换
    • 基本数据类型 <-> 包装类:自动装箱、自动拆箱(从 JDK5.0 开始)
    • 基本数据类型、包装类 <-> String
      • String的valueOf(xxx)
      • 包装类的parseXxx(String str)
  • 包装类的其它 API
  1. 数据类型的最大最小值:Integer.MAX_VALUE 和 Integer.MIN_VALUE; Long.MAX_VALUE 和 Long.MIN_VALUE; Double.MAX_VALUE 和 Double.MIN_VALUE
  2. 字符转大小写:Character.toUpperCase(‘x’); Character.toLowerCase(‘X’);
  3. 整数转进制:Integer.toBinaryString(int i); Integer.toHexString(int i); Integer.toOctalString(int i)
  4. 比较的方法:Double.compare(double d1, double d2);Integer.compare(int x, int y)
  • 面试题:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public void method1() {
    Integer i = new Integer(1);
    Integer j = new Integer(1);
    System.out.println(i == j);//false
    //底层都会调用Integer的valueOf()
    Integer m = 1;
    Integer n = 1;
    System.out.println(m == n);//true 看源码,如果在[-128,127]间,从现有数组里取,(享元模式)
    Integer x = 128;
    Integer y = 128;
    System.out.println(x == y);//false
    }

    Object o1 = true ? new Integer(1) : new Double(2.0);//三目运算符后面类型必须一致
    System.out.println(o1);//1.0

企业真题(八)

2.1 static关键字

1. 静态变量和实例变量的区别?(保*丰、*软国际、*软华*、北京明**信)

1
2
类似问题:
> 说明静态变量和实例变量之间的区别和使用场景(上海*动)

2. 静态属性和静态方法是否可以被继承?是否可以被重写?以及原因?(*度)

1
2
类似问题:
> 在java中,可以重载一个static方法吗?可以覆盖一个static方法吗?(Vi*o)

静态方法不能被重写。不存在多态性。

3. 是否可以从一个static方法内部发出对非static方法的调用?(同*顺)

只能通过对象来对非静态方法的调用。

4. 被static修饰的成员(类、方法、成员变量)能否再使用private进行修饰?(联*优势)

完全可以。除了代码块。

2.2 设计模式

1. 知道哪些设计模式?(*通快递、蚂**服)

单例模式、模板方法、享元设计模式

2. 开发中都用到了那些设计模式?用在什么场合? (久*国际物流)

2.3 main()

1. main()方法的public能不能换成private,为什么(凡*科技、顺*)

可以改。但是改完以后就不是程序入口了。

2. main()方法中是否可以调用非静态方法?(浩*科技)

只能通过对象来对非静态方法的调用。

2.4 代码块

1. 类的组成和属性赋值执行顺序?(航*拓普)

1
2
类似问题:
> Java中类的变量初始化的顺序?(*壳)

略。

2. 静态代码块,普通代码块,构造方法,从类加载开始的执行顺序?(恒*电子)

1
2
3
4
类似问题:
> 类加载成员变量、静态代码块、构造器的加载顺序(*科软、软**力、同*顺)
> static代码块(静态代码块)是否在类的构造函数之前被执行(联*优势)

静态代码块 –> 普通代码块 –> 构造器

2.5 final关键字

1. 描述一下对final理解(华**博普)

2. 判断题:使用final修饰一个变量时,是引用不能改变,引用指向的对象可以改变?(*米)

引用不能改变。

引用指向的对象实体中的属性,如果没有使用final修饰,则可以改变。

3. 判断题:final不能用于修饰构造方法?(联*优势)

是的。

4. final或static final 修饰成员变量,能不能进行++操作?(佳*贸易)

不能。

2.6 抽象类与接口

1. 什么是抽象类?如何识别一个抽象类?(易*支付)

使用abstract修饰。

2. 为什么不能用abstract修饰属性、私有方法、构造器、静态方法、final的方法?(止**善)

略。 为了语言的自洽。

3. 接口与抽象类的区别?(字*跳动、阿*校招、*度校招、**计算机技术及应用研究所、航*拓普、纬*、招**晟、汇*云通、数信**科技、北京永*鼎力、上海*连科技)

略。

4. 接口是否可继承接口?抽象类是否可实现(implements)接口?抽象类是否可继承实现类(concrete class)?(航*拓普、*蝶、深圳德*科技)

1
2
类似问题:
> 接口A可以继承接口B吗?接口A可以实现接口B吗?(久*国际物流)

是;是;是;

5. 接口可以有自己属性吗?(华*中盛)

可以。必须是public static final的

6. 访问接口的默认方法如何使用(上海*思)

使用实现类的对象进行调用。而且实现还可以重写此默认方法。

2.7 内部类

1. 内部类有哪几种?(华油**普、来*科技)

略。

2. 内部类的特点说一下(招通**)

1
2
3
类似问题:
> 说一下内部类的好处(北京楚*龙)
> 使用过内部类编程吗,有什么作用(软**力)

8.匿名类说一下(阿*校招、上海立*网络)

2.8 枚举类

1. 枚举可以继承吗?(顺*)

使用enum定义的,其父类就是Enum类,就不要再继承其他的类了。

2.9 包装类

1. Java基本类型与包装类的区别(凡*科技)

略。

2.10 综合

1. 谈谈你对面向对象的理解(君*科技、航*拓普、…)

  • 面向对象的两个要素:类、对象 —> 面向对象编程。“万事万物皆对象”。
  • 面向对象的三大特征
  • 接口,与类并列的结构,作为一个补充:类可以实现多个接口。

2. 面向对象的特征有哪些方面? (北京楚*龙、深圳德*科技、直*科技、米*奇网络、航*拓普)

1
2
类似问题:
> 面向对象核心是什么?(平**服)

异常处理

异常的概述

1
2
3
4
5
6
7
8
9
10
11
12
1. 什么是异常?
指的是程序在执行过程中,出现的非正常情况,如果不处理最终会导致JVM的非正常停止。

2. 异常的抛出机制
Java中把不同的异常用不同的类表示,一旦发生某种异常,就`创建该异常类型的对象`,并且抛出(throw)。
然后程序员可以捕获(catch)到这个异常对象,并处理;如果没有捕获(catch)这个异常对象,那么这个异常
对象将会导致程序终止。

3. 如何对待异常
对于程序出现的异常,一般有两种解决方法:一是遇到错误就终止程序的运行。另一种方法是程序员在编写程序时,
就充分考虑到各种可能发生的异常和错误,极力预防和避免。实在无法避免的,要编写相应的代码进行异常的检测、
以及`异常的处理`,保证代码的`健壮性`。

异常的体系结构及常见的异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java.lang.Throwable:异常体系的根父类
• public void printStackTrace():打印异常的详细信息。
包含了异常的类型、异常的原因、异常出现的位置、在开发和调试阶段都得使用printStackTrace。
• public String getMessage():获取发生异常的原因。
|---java.lang.Error:错误。Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况。
一般不编写针对性的代码进行处理。
|---- StackOverflowError(栈内存溢出)和 OutOfMemoryError(堆内存溢出,简称OOM)

|---java.lang.Exception:异常。我们可以编写针对性的代码进行处理。
|----编译时异常:(受检异常)在执行javac.exe命令时,出现的异常。
|----- ClassNotFoundException
|----- FileNotFoundException
|----- IOException
|----运行时异常:(非受检异常)在执行java.exe命令时,出现的异常。
|---- ArrayIndexOutOfBoundsException
|---- NullPointerException
|---- ClassCastException
|---- NumberFormatException
|---- InputMismatchException
|---- ArithmeticException
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
【面试题】说说你在开发中常见的异常都有哪些?

开发1-2年:
|----编译时异常:(受检异常)在执行javac.exe命令时,出现的异常。
|----- ClassNotFoundException
|----- FileNotFoundException
|----- IOException
|----运行时异常:(非受检异常)在执行java.exe命令时,出现的异常。
|---- ArrayIndexOutOfBoundsException
|---- NullPointerException
|---- ClassCastException
|---- NumberFormatException
|---- InputMismatchException
|---- ArithmeticException

开发3年以上:
OOM。

异常处理的方式

1
2
3
4
5
6
7
8
9
过程1:“抛”
>"自动抛" : 程序在执行的过程当中,一旦出现异常,就会在出现异常的代码处,自动生成对应异常类的对象,并将此对象抛出。

>"手动抛" :程序在执行的过程当中,不满足指定条件的情况下,我们主动的使用"throw + 异常类的对象"方式抛出异常对象。


过程2:“抓”
狭义上讲:try-catch的方式捕获异常,并处理。
广义上讲:把“抓”理解为“处理”。则此时对应着异常处理的两种方式:① try-catch-finally ② throws

try-catch-finally

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
1. 基本结构:
try{
...... //可能产生异常的代码
}
catch( 异常类型1 e ){
...... //当产生异常类型1型异常时的处置措施
}
catch( 异常类型2 e ){
...... //当产生异常类型2型异常时的处置措施
}
finally{
...... //无论是否发生异常,都无条件执行的语句
}

2. 使用细节:
> 将可能出现异常的代码声明在try语句中。一旦代码出现异常,就会自动生成一个对应异常类的对象。并将此对象抛出。
> 针对于try中抛出的异常类的对象,使用之后的catch语句进行匹配。一旦匹配上,就进入catch语句块进行处理。
一旦处理接触,代码就可继续向下执行。
> 如果声明了多个catch结构,不同的异常类型在不存在子父类关系的情况下,谁声明在上面,谁声明在下面都可以。
如果多个异常类型满足子父类的关系,则必须将子类声明在父类结构的上面。否则,报错。
> catch中异常处理的方式:
① 自己编写输出的语句。
② printStackTrace():打印异常的详细信息。 (推荐)
③ getMessage():获取发生异常的原因。
> try中声明的变量,出了try结构之后,就不可以进行调用了。
> try-catch结构是可以嵌套使用的。
1
2
3
4
5
6
7
8
9
10
11
12
13
3. finally的使用说明:
3.1 finally的理解
> 我们将一定要被执行的代码声明在finally结构中。
> 更深刻的理解:无论try中或catch中是否存在仍未被处理的异常,无论try中或catch中是否存在return语句等,finally
中声明的语句都一定要被执行。

> finally语句和catch语句是可选的,但finally不能单独使用。

3.2 什么样的代码我们一定要声明在finally中呢?
> 我们在开发中,有一些资源(比如:输入流、输出流,数据库连接、Socket连接等资源),在使用完以后,必须显式的进行
关闭操作,否则,GC不会自动的回收这些资源。进而导致内存的泄漏。
为了保证这些资源在使用完以后,不管是否出现了未被处理的异常的情况下,这些资源能被关闭。我们必须将这些操作声明
在finally中!

throws

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1. 格式:在方法的声明处,使用"throws 异常类型1,异常类型2,..."

2. 举例:

public void test() throws 异常类型1,异常类型2,.. {
//可能存在编译时异常
}


3. 是否真正处理了异常?
> 从编译是否能通过的角度看,看成是给出了异常万一要是出现时候的解决方案。此方案就是,继续向上抛出(throws)。
> 但是,此throws的方式,仅是将可能出现的异常抛给了此方法的调用者。此调用者仍然需要考虑如何处理相关异常。
从这个角度来看,throws的方式不算是真正意义上处理了异常。


4. 方法的重写的要求:(针对于编译时异常来说的)
子类重写的方法抛出的异常类型可以与父类被重写的方法抛出的异常类型相同,或是父类被重写的方法抛出的异常类型的子类。

开发中的经验之谈:

1
2
3
4
5
6
7
开发中,如何选择异常处理的两种方式?(重要、经验之谈)
- 如果程序代码中,涉及到资源的调用(流、数据库连接、网络连接等),则必须考虑使用try-catch-finally来处理,
保证不出现内存泄漏。
- 如果父类被重写的方法没有throws异常类型,则子类重写的方法中如果出现异常,只能考虑使用try-catch-finally
进行处理,不能throws。
- 开发中,方法a中依次调用了方法b,c,d等方法,方法b,c,d之间是递进关系。此时,如果方法b,c,d中有异常,
我们通常选择使用throws,而方法a中通常选择使用try-catch-finally。

手动throw异常对象

1
在方法内部,满足指定条件的情况下,使用"throw 异常类的对象"的方式抛出。

如何自定义异常类

1
2
3
① 继承于现有的异常体系。通常继承于RuntimeException \ Exception
② 通常提供几个重载的构造器
③ 提供一个全局常量,声明为:static final long serialVersionUID;
1
2
3
为什么需要自定义异常类?
我们其实更关心的是,通过异常的名称就能直接判断此异常出现的原因。既然如此,我们就有必要在实际开发场景中,
不满足我们指定的条件时,指明我们自己特有的异常类。通过此异常类的名称,就能判断出具体出现的问题。

企业真题(九)

2.1 异常概述

1. Java的异常体系简单介绍下(网*)

1
2
3
包含问题:
> 4.异常的顶级接口是什么(软**力)
> 异常类的继承关系,exception下都有哪些类?(上海*冉信息)

2. Java异常处理机制(*科软)

两种处理方案:try-catch-finally ;throws

3. 异常的两种类型,Error和Exception的区别(上海冠*新创、北京中**译、*度)

4. 运行时异常与一般异常有何异同?(华*思为)

运行时异常:RuntimeException

  • 编译可以通过。在运行时可能抛出。出现的概率高一些;一般针对于运行时异常,都不处理。

一般异常:Exception

  • 编译不能通过。要求必须在编译之前,考虑异常的处理。不处理编译不通过。

5. 说几个你常见到的异常(华油**普)

1
2
3
类似问题:
> 请列出Java中常见的几种异常?(百*园)
> 给我一个你最常见到的runtime exception。(*蝶)

2.2 try-catch-finally

1. 说说final、finally、finalize的区别(北京中**译、艾*软件、拓*思、*科软)

1
2
类似问题:
> 1. finally和final的区别(*科软)

略。

2. 如果不使用try-catch,程序出现异常会如何?(上海冠*新创科技)

对于当前方法来讲,如果不使用try-catch,则在出现异常对象以后会抛出此对象。如果没有处理方案,就会终止程序的执行。

3. try … catch捕捉的是什么异常?(北京亿*东方)

Exception。非Error

4. 如果执行finally代码块之前方法返回了结果或者jvm退出了,这时finally块中的代码还会执行吗?(恒*电子)

特别的:System.exit(0);

5. 在try语句中有return语句,最后写finally语句,finally语句中的code会不会执行?何时执行?如果执行是在return前还是后(拓*思、华**为)

6. 捕获异常在catch块里一定会进入finally吗?catch里能return吗?catch里return还会进finally吗?在try里return是什么情况?(*蓝)

2.3 throw与throws

1. throw和throws的区别?(北京亿**方、北京新*阳光)

角度1:”形”,即使用的格式

1
2
throw:使用在方法内部,“throw 异常类的对象”
throws:使用在方法的声明处,"throws 异常类1,异常类2,..."

角度2:”角色”或作用不同。

1
2
3
4
5
6
上游排污,下游治污。

过程1:“抛”
>throw
过程2:“抓”
> try-catch ; throws

2. 子类重写父类抛出异常的方法,能否抛出比父类更高级别的异常类(顺*)

不能!

2.4 自定义异常

1. 如何自定义一个异常?(*软国际)

多线程

几个概念

1
2
3
4
5
6
7
程序(program):为完成特定任务,用某种语言编写的`一组指令的集合`。即指一段静态的代码。

进程(process):程序的一次执行过程,或是正在内存中运行的应用程序。程序是静态的,进程是动态的。
进程作为操作系统调度和分配资源的最小单位。

线程(thread):进程可进一步细化为线程,是程序内部的一条执行路径。
线程作为CPU调度和执行的最小单位
1
2
3
4
线程调度策略
分时调度:所有线程`轮流使用` CPU 的使用权,并且平均分配每个线程占用 CPU 的时间。

抢占式调度:让`优先级高`的线程以`较大的概率`优先使用 CPU。如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
1
2
3
4
5
6
7
> 单核CPU与多核CPU
• 单核 CPU,在一个时间单元内,只能执行一个线程的任务。
> 并行与并发
• 并行(parallel):指两个或多个事件在同一时刻发生(同时发生)。指在同一时刻,有多条指令在多个 CPU 上同时执行。比如:多个人同时做不同的事。
• 并发(concurrency):指两个或多个事件在同一个时间段内发生。即在一段时间
内,有多条指令在单个 CPU 上快速轮换、交替执行,使得在宏观上具有多个进程同时执行的效果。

2. 如何创建多线程(重点)

Java 语言的 JVM 允许程序运行多个线程,使用 java.lang.Thread 类代表线程,所有的线程对象都必须是 Thread 类或其子类的实例。

  • 方式1:继承Thread类
    Thread 类的特性:
    1. 每个线程都是通过某个特定 Thread 对象的 run()方法来完成操作的,因此
      把 run()方法体称为线程执行体。
    2. 通过该 Thread 对象的 start()方法来启动这个线程,而非直接调用 run()
    3. 要想实现多线程,必须在主线程中创建新的线程对象。
  • 方式2:实现Runnable接口
    • Java 有单继承的限制,当我们无法继承 Thread 类时,那么该如何做呢?在核心
      类库中提供了 Runnable 接口,我们可以实现 Runnable 接口,重写 run()方法,
      然后再通过 Thread 类的对象代理启动和执行我们的线程体 run()方法
      1
      2
      3
      4
      5
      6

      //创建自定义类对象 线程任务对象
      MyRunnable mr = new MyRunnable();
      //创建线程对象
      Thread t = new Thread(mr, "长江");

    • 使用匿名内部类对象来实现线程的创建和启动
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      new Thread("新的线程!"){
      @Override
      public void run() {
      for (int i = 0; i < 10; i++) {
      System.out.println(getName()+":正在执行!"+i);
      }
      }
      }.start();
      new Thread(new Runnable(){
      @Override
      public void run() {
      for (int i = 0; i < 10; i++) {
      System.out.println(Thread.currentThread().getName()+":" + i);
      }
      }
      }).start();

  • 方式3:实现Callable接口 (jdk5.0新增)
    • 与使用 Runnable 相比, Callable 功能更强大些
      1. 相比 run()方法,可以有返回值
      2. 方法可以抛出异常
      3. 支持泛型的返回值(需要借助 FutureTask 类,获取返回结果)
    • Future 接口(了解)
      1. 可以对具体 Runnable、Callable 任务的执行结果进行取消、查询是否完成、获取结果等。
      2. FutureTask 是 Futrue 接口的唯一的实现类
      3. FutureTask 同时实现了 Runnable, Future 接口。它既可以作为Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值
    • 缺点:在获取分线程执行结果的时候,当前线程(或是主线程)受阻塞,效率较低。
  • 方式4:使用线程池(jdk5.0新增)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    • ExecutorService:真正的线程池接口。常见子类 ThreadPoolExecutor
    void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行 Runnable
    – <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行 Callable
    void shutdown() :关闭连接池
    • Executors:一个线程池的工厂类,通过此类的静态工厂方法可以创建多种类型的线程池对象。
    – Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
    – Executors.newFixedThreadPool(int nThreads); 创建一个可重用固定线程数的线程池
    – Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
    – Executors.newScheduledThreadPool(int corePoolSize):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

3. Thread类的常用方法、线程的生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
熟悉常用的构造器和方法:
1. 线程中的构造器
- public Thread() :分配一个新的线程对象。
- public Thread(String name) :分配一个指定名字的新的线程对象。
- public Thread(Runnable target) :指定创建线程的目标对象,它实现了Runnable接口中的run方法
- public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

2.线程中的常用方法:
> start():①启动线程 ②调用线程的run()
> run():将线程要执行的操作,声明在run()中。
> currentThread():获取当前执行代码对应的线程
> getName(): 获取线程名
> setName(): 设置线程名
> sleep(long millis):静态方法,调用时,可以使得当前线程睡眠指定的毫秒数
> yield():静态方法,一旦执行此方法,就释放CPU的执行权,让系统的线程调度器重新调度一次
> join(): 在线程a中通过线程b调用join(),意味着线程a进入阻塞状态,直到线程b执行结束,线程a才结束阻塞状态,继续执行。
> isAlive():判断当前线程是否存活

3. 线程的优先级:
getPriority():获取线程的优先级
setPriority():设置线程的优先级。范围[1,10]


Thread类内部声明的三个常量:
- MAX_PRIORITY(10):最高优先级
- MIN _PRIORITY (1):最低优先级
- NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级。

线程的生命周期:

jdk5.0之前:

线程的生命周期有五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)

jdk5.0及之后:Thread类中定义了一个内部类State

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum State {

NEW,

RUNNABLE,

BLOCKED,

WAITING,

TIMED_WAITING,

TERMINATED;
}

4. 如何解决线程安全问题(重点、难点)

  • 什么是线程的安全问题?多个线程操作共享数据,就有可能出现安全问题。

  • 如何解决线程的安全问题?有几种方式?

    • 同步机制:① 同步代码块 ② 同步方法
    1. 同步代码块:synchronized 关键字可以用于某个区块前面,表示只对这个区块

的资源实行互斥访问

1
2
3
synchronized(同步锁){
需要同步操作的代码
}
  1. 同步方法:synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能
    进入这个方法,其他线程在外面等着。
    1
    2
    3
    4
    public synchronized void method(){
    可能会产生线程安全问题的代码
    }

- 重点关注两个事:共享数据及操作共享数据的代码;同步监视器(保证唯一性)

1
2
在实现Runnable接口的方式中,同步监视器可以考虑使用:this。
在继承Thread类的方式中,同步监视器要慎用this,可以考虑使用:当前类.class。
1
2
非静态的同步方法,默认同步监视器是this
静态的同步方法,默认同步监视器是当前类本身。
  • jdk5.0新增:Lock接口及其实现类。(保证多个线程共用同一个Lock的实例)
    Lock 锁也称同步锁,加锁与释放锁方法,如下:
    • public void lock() :加同步锁。
    • public void unlock() :释放同步锁。
    • synchronized 与 Lock 的对比
      1. Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized 是隐式锁,出了
        作用域、遇到异常等自动解锁
      2. Lock 只有代码块锁,synchronized 有代码块锁和方法锁
      3. 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性
        (提供更多的子类),更体现面向对象。
      4. (了解)Lock 锁可以对读不加锁,对写加锁,synchronized 不可以
      5. (了解)Lock 锁可以有多种获取锁的方式,可以从 sleep 的线程中抢到锁,
        synchronized 不可以
        说明:开发建议中处理线程安全问题优先使用顺序为:
        Lock —-> 同步代码块 —-> 同步方法

5. 同步机制相关的问题

  • 懒汉式的线程安全的写法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    public class LazyOne {
    private static LazyOne instance;
    private LazyOne(){}
    //方式 1:
    public static synchronized LazyOne getInstance1(){
    if(instance == null){
    instance = new LazyOne();
    }
    return instance;
    }
    //方式 2:
    public static LazyOne getInstance2(){
    synchronized(LazyOne.class) {
    if (instance == null) {
    instance = new LazyOne();
    }
    return instance;
    }
    }
    //方式 3:
    public static LazyOne getInstance3(){
    if(instance == null){
    synchronized (LazyOne.class) {
    try {
    Thread.sleep(10);//加这个代码,暴露问题
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    if(instance == null){
    instance = new LazyOne();
    }
    }
    }
    return instance;
    }
    /*
    注意:上述方式 3 中,有指令重排问题
    mem = allocate(); 为单例对象分配内存空间
    instance = mem; instance 引用现在非空,但还未初始化
    ctorSingleton(instance); 为单例对象通过 instance 调用构造器
    从 JDK2 开始,分配空间、初始化、调用构造器会在线程的工作存储区一次性完
    成,然后复制到主存储区。但是需要
    volatile 关键字,避免指令重排。
    */

    }

  • 同步机制会带来的问题:死锁
    • 死锁产生的条件及规避方式
      • 互斥条件
      • 占用且等待
      • 不可抢夺(或不可抢占)
      • 循环等待
      以上 4 个条件,同时出现就会触发死锁。

6. 线程间的通信

  • 在同步机制下,考虑线程间的通信

  • wait() 、notify() 、notifyAll() 都需要使用在同步代码块或同步方法中。

  • 高频笔试题:wait() / sleep()
    相同点:一旦执行,都会使得当前线程结束执行状态,进入阻塞状态。
    不同点:
    ① 定义方法所属的类:sleep():Thread 中定义。 wait():Object 中定义
    ② 使用范围的不同:sleep()可以在任何需要使用的位置被调用; wait():必须使
    用在同步代码块或同步方法中
    ③ 都在同步结构中使用的时候,是否释放同步监视器的操作不同:sleep():不会
    释放同步监视器 ;wait():会释放同步监视器
    ④ 结束等待的方式不同:sleep():指定时间一到就结束阻塞。 wait():可以指定
    时间也可以无限等待直到 notify 或 notifyAll。

企业真题(十)

2.1 线程概述

1. 什么是线程(*云网络)

2. 线程和进程有什么区别(*团、腾*、*云网络、神**岳、言*有物、直*科技)

进程:对应一个运行中的程序。

线程:运行中的进程的一条或多条执行路径。

3. 多线程使用场景(嘉*医疗)

  • 手机app应用的图片的下载
  • 迅雷的下载
  • Tomcat服务器上web应用,多个客户端发起请求,Tomcat针对多个请求开辟多个线程处理

2.2 如何实现多线程

1. 如何在Java中出实现多线程?(阿*校招、当*置业、鸿*网络、奥*医药、*科软、慧*、上海驿*软件、海*科)

1
2
3
类似问题:
> 创建多线程用Runnable还是Thread(北京中*瑞飞)
> 多线程有几种实现方法,都是什么?(锐*(上海)企业管理咨询)

四种。

2. Thread类中的start()和run()有什么区别?(北京中油**、爱*信、神*泰岳、直*科技,*软国际,上海*学网络)

start():① 开启线程 ② 调用线程的run()

3. 启动一个线程是用run()还是start()?(*度)

start()

4. Java中Runnable和Callable有什么不同?(平*金服、银*数据、好*在、亿*征信、花儿**网络)

1
2
3
4
5
6
与之前的方式的对比:与Runnable方式的对比的好处
> call()可以有返回值,更灵活
> call()可以使用throws的方式处理异常,更灵活
> Callable使用了泛型参数,可以指明具体的call()的返回值类型,更灵活

有缺点吗?如果在主线程中需要获取分线程call()的返回值,则此时的主线程是阻塞状态的。

5. 什么是线程池,为什么要使用它?(上海明*物联网科技)

1
2
3
4
此方式的好处:
> 提高了程序执行的效率。(因为线程已经提前创建好了)
> 提高了资源的复用率。(因为执行完的线程并未销毁,而是可以继续执行其他的任务)
> 可以设置相关的参数,对线程池中的线程的使用进行管理

2.3 常用方法、生命周期

1. sleep() 和 yield()区别?(神*泰岳)

sleep():一旦调用,就进入“阻塞”(或TIMED_WAITING状态)

yield():释放cpu的执行权,处在RUNNABLE的状态

2. 线程创建中的方法、属性情况?(招通**、数*互融)

3. 线程的生命周期?(中国**电子商务中心、*科软、慧*)

4. 线程的基本状态以及状态之间的关系?(直*科技)

1
2
3
4
类似问题:
> 线程有哪些状态?如何让线程进入阻塞?(华*中*,*兴)
> 线程有几个状态,就绪和阻塞有什么不同。(美*)
> Java的线程都有哪几种状态(字*跳动、*东、*手)

5. stop()和suspend()方法为何不推荐使用?(上海驿*软件)

stop():一旦执行,线程就结束了,导致run()有未执行结束的代码。stop()会导致释放同步监视器,导致线程安全问题。

suspend():与resume()搭配使用,导致死锁。

6. Java 线程优先级是怎么定义的?(软*动力)

三个常量。[1,10]

2.4 线程安全与同步机制

1. 你如何理解线程安全的?线程安全问题是如何造成的?(*软国际)

1
2
3
4
类似问题:
> 线程安全说一下?(奥*医药)
> 对线程安全的理解(*度校招)
> 什么是线程安全?(银*数据)

2. 多线程共用一个数据变量需要注意什么?(史*夫软件)

线程安全问题

3. 多线程保证线程安全一般有几种方式?(来*科技、北京*信天*)

1
2
3
4
5
6
7
8
9
类似问题:
> 如何解决其线程安全问题,并且说明为什么这样子去解决?(北京联合**)
> 请说出你所知道的线程同步的方法。(天*伟业)
> 哪些方法实现线程安全?(阿*)
> 同步有几种实现方法,都是什么? (锐*企业管理咨询)
> 你在实际编码过程中如何避免线程安全问题?(*软国际)
> 如何让线程同步?(*手)
> 多线程下有什么同步措施(阿*校招)
> 同步有几种实现方法,都是什么?(海*科)
  • 同步机制
  • Lock接口

4. 用什么关键字修饰同步方法?(上海驿*软件)

synchronized

5. synchronized加在静态方法和普通方法区别(来*科技)

同步监视器不同。静态:当前类本身 非静态:this

6. Java中synchronized和ReentrantLock有什么不同(三*重工)

1
2
3
4
类似问题:
> 多线程安全机制中 synchronized和lock的区别(中*国际、*美、鸿*网络)
> 怎么实现线程安全,各个实现方法有什么区别?(美*、字*跳动)
> synchronized 和 lock 区别(阿*、*壳)
1
2
3
synchronized不管是同步代码块还是同步方法,都需要在结束一对{}之后,释放对同步监视器的调用。
Lock是通过两个方法控制需要被同步的代码,更灵活一些。
Lock作为接口,提供了多种实现类,适合更多更复杂的场景,效率更高。

7. 当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法?(鸿*网络)

需要看其他方法是否使用synchronized修饰,同步监视器的this是否是同一个。

只有当使用了synchronized,且this是同一个的情况下,就不能访问了。

8. 线程同步与阻塞的关系?同步一定阻塞吗?阻塞一定同步吗?(阿*校招、西安*创佳*)

同步一定阻塞;阻塞不一定同步。

2.5 死锁

1. 什么是死锁,产生死锁的原因及必要条件(腾*、阿*)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1. 如何看待死锁?
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
我们编写程序时,要避免出现死锁。

2. 诱发死锁的原因?
- 互斥条件
- 占用且等待
- 不可抢夺(或不可抢占)
- 循环等待

以上4个条件,同时出现就会触发死锁。


3. 如何避免死锁?
针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。
针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。

2. 如何避免死锁?(阿*、北京*蓝、*手)

见上。

2.6 线程通信

1. Java中notify()和notifyAll()有什么区别(汇*天下)

1
2
3
notify():一旦执行此方法,就会唤醒被wait()的线程中优先级最高的那一个线程。(如果被wait()的多个线程的优先级相同,则
随机唤醒一个)。被唤醒的线程从当初被wait的位置继续执行。
notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。

2. 为什么wait()和notify()方法要在同步块中调用(北京*智)

因为调用者必须是同步监视器。

3. 多线程:生产者,消费者代码(同步、wait、notifly编程)(猫*娱乐)

1
2
3
4
类似问题:
> 如何写代码来解决生产者消费者问题(上海明*物联网)
> 多线程中生产者和消费者如何保证同步(*为)
> 消费者生产者,写写伪代码(字*)

4. wait()和sleep()有什么区别?调用这两个函数后,线程状态分别作何改变?(字*、*东)

1
2
3
4
类似问题:
> 线程中sleep()和wait()有什么区别?(外派*度)
> Java线程阻塞调用 wait 函数和 sleep 区别和联系(阿*)
> wait和sleep的区别,他们两个谁会释放锁(软*动力、*创)
1
2
3
4
5
6
7
8
9
10
11
相同点:一旦执行,当前线程都会进入阻塞状态

不同点:
> 声明的位置:wait():声明在Object类中
sleep():声明在Thread类中,静态的
> 使用的场景不同:wait():只能使用在同步代码块或同步方法中
sleep():可以在任何需要使用的场景
> 使用在同步代码块或同步方法中:wait():一旦执行,会释放同步监视器
sleep():一旦执行,不会释放同步监视器
> 结束阻塞的方式:wait(): 到达指定时间自动结束阻塞 或 通过被notify唤醒,结束阻塞
sleep(): 到达指定时间自动结束阻塞

2.7 单例模式(线程安全)

1. 手写一个单例模式(Singleton),还要安全的(*通快递、君*科技)

饿汉式;安全的懒汉式;内部类;

2. 手写一个懒汉式的单例模式&解决其线程安全问题,并且说明为什么这样子去解决(5*)

1
2
类似问题:
> 手写一个懒汉式的单例模式(北京联合**)

同上。

常用类与基础API

String类

java.lang.String,字符串是常量,用双引号引起来表示。它们的值在创建之后不能更改

  • String的声明:final修饰、实现了Comparable接口
  • String的不可变性
    1. 当对字符串变量重新赋值时,需要重新指定一个字符串常量的位置进行赋值,不能在原有的位置修改
    2. 当对现有的字符串进行拼接或replace()操作时,需要重新开辟空间保存操作后的字符串,不能在原有的位置修改
  • String的两种定义方式:① 字面量的定义方式 String s = “hello” ② new 的方式:String s = new String(“hello”);
    • String的内存解析:字符串常量池、堆内存的使用
    • String s = new String(“hello”);在内存中创建的对象的个数。→ 创建了两个对象
  • String的连接操作:+
    • 常量 + 常量:结果仍然存储在字符串常量池中,返回此字面量地址,此时的常量可能是字面量,也可能是final修饰的变量 、变量 + 常量 or 变量 + 变量:都会通过new的方式创建一个新的字符串,返回堆空间中此字符串对象的地址、concat(String otherString):调用完都返回一个新new的对象
    • String intern():返回的是字符串常量池中字面量的地址
  • 熟悉String的构造器、与其他结构之间的转换
    • 字符串 –> 基本数据类型、包装类 public static int parseInt(String s)
    • 基本数据类型、包装类 –> 字符串 public String valueOf(int n)
    • 字符串 –> 字符数组 public char[] toCharArray():将字符串中的全部字符存放在一个字符数组中的方法。public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin):提供了将指定索引范围内的字符串存放到数组中的方法
    • 字符数组 –> 字符串 String 类的构造器
    • 编码和解码
      • 编码:字符、字符串 –> 字节、字节数组。对应着编码集
      • 解码:字节、字节数组 –> 字符、字符串。对应着解码集 在utf-8字符集中,一个汉字占用3个字节,一个字母1个字节,在gbk字符集中,一个汉字占用2个字节,一个字母1个字节 均向下兼容ASCII码
      • 规则:解码集必须使用当初编码时使用的编码集。只要不一致,就可能出现乱码!
  • String常用方法
    1. boolean isEmpty():字符串是否为空
    2. int length():返回字符串的长度
    3. String concat(xx):拼接
    4. boolean equals(Object obj):比较字符串是否相等,区分大小写
    5. boolean equalsIgnoreCase(Object obj):比较字符串是否相等,不区分大小写
    6. int compareTo(String other):比较字符串大小,区分大小写,按照 Unicode 编码值比较大小
    7. int compareToIgnoreCase(String other):比较字符串大小,不区分大小写
    8. String toLowerCase():将字符串中大写字母转为小写
    9. String toUpperCase():将字符串中小写字母转为大写
    10. String trim():去掉字符串前后空白符
    11. public String intern():结果在常量池中共享
    12. boolean contains(xx):是否包含 xx
    13. int indexOf(xx):从前往后找当前字符串中 xx,即如果有返回第一次出现的下标,要是没有返回-1
    14. int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始
    15. int lastIndexOf(xx):从后往前找当前字符串中 xx,即如果有返回最后一次出现的下标,要是没有返回-1
    16. int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索。
    17. String substring(int beginIndex) :返回一个新的字符串,它是此字符串
      的从 beginIndex 开始截取到最后的一个子字符串。
    18. String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字符串从 beginIndex 开始截取到 endIndex(不包含)的一个子字符串。
    19. char charAt(index):返回index位置的字符
    20. char[] toCharArray(): 将此字符串转换为一个新的字符数组返回
    21. static String valueOf(char[] data) :返回指定数组中表示该字符序列的 String
    22. static String valueOf(char[] data, int offset, int count) : 返回指定数组中表示该字符序列的 String
    23. static String copyValueOf(char[] data): 返回指定数组中表示该字符序列的 String
    24. static String copyValueOf(char[] data, int offset, int count):返回指定数组中表示该字符序列的 String
    25. boolean startsWith(xx):测试此字符串是否以指定的前缀开始
    26. boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始
    27. boolean endsWith(xx):测试此字符串是否以指定的后缀结束
    28. String replace(char oldChar, char newChar):返回一个新的字符串,它是
      通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。 不支持正则。
    29. String replace(CharSequence target, CharSequence replacement):
      使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。
    30. String replaceAll(String regex, String replacement):使用给定的replacement 替换此字符串所有匹配给定的正则表达式的子字符串。
    31. String replaceFirst(String regex, String replacement):使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。
  • String相关的算法问题。
    1. 模拟一个 trim 方法,去除字符串两端的空格。
    2. 将一个字符串进行反转。将字符串中指定部分进行反转。比如“abcdefg”反转为”abfedcg”
    3. 获取一个字符串在另一个字符串中出现的次数。 比如:获取“ ab”在“abkkcadkabkebfkabkskab” 中出现的次数
    4. 获取两个字符串中最大相同子串。比如: str1 = “abcwerthelloyuiodef”;str2 = “cvhellobnm” 提示:将短的那个串进行长度依次递减的子串与较长的串比较。
    5. 对字符串中字符进行自然顺序排序。 提示: 1)字符串变成字符数组。 2)对数组排序,选择,冒泡,Arrays.sort(); 3)将排序后的数组变成字符串。

StringBuffer、StringBuilder类

因为 String 对象是不可变对象,虽然可以共享常量对象,但是对于频繁字符串的修改和拼接操作,效率极低,空间消耗也比较高。因此,JDK 又在 java.lang包提供了可变字符序列 StringBuffer 和 StringBuilder 类型。

  • [面试题]String、StringBuffer、StringBuilder的区别

    String:不可变的字符序列;底层使用char[] (jdk8及之前),底层使用byte[] (jdk9及之后)
    StringBuffer:可变的字符序列;JDK1.0声明,线程安全的,效率低;底层使用char[] (jdk8及之前),底层使用byte[] (jdk9及之后)
    StringBuilder:可变的字符序列;JDK5.0声明,线程不安全的,效率高;底层使用char[] (jdk8及之前),底层使用byte[] (jdk9及之后)

  • 知道什么场景下使用StringBuffer、StringBuilder

    如果开发中需要频繁的针对于字符串进行增、删、改等操作,建议使用StringBuffer或StringBuilder替换String.
    因为使用String效率低。
    如果开发中,不涉及到线程安全问题,建议使用StringBuilder替换StringBuffer。因为使用StringBuilder效率高
    如果开发中大体确定要操作的字符的个数,建议使用带int capacity参数的构造器。因为可以避免底层多次扩容操作,性能更高。

  • StringBuffer和StringBuilder中的常用方法
    增:
    append(xx)
    删:
    delete(int start, int end)
    deleteCharAt(int index)
    改:
    replace(int start, int end, String str)
    setCharAt(int index, char c)
    查:
    charAt(int index)
    插:
    insert(int index, xx)
    长度:
    length()

  • 执行效率:StringBuilder > StringBuffer > String(从高到低)

jdk8之前的日期、时间API

  • System的currentTimeMillis()
  • 两个Date的使用
    1. java.util.Date:getTime(),toString(),很多过时的方法
    2. java.sql.Date
  • java.text.SimpleDateFormat,SimpleDateFormat类是一个不与语言环境有关的方式来格式化和解析日期的具体类
  • Calendar日历类的使用,使用 Calendar.getInstance()方法获取 Calendar 实例
    • public int get(int field):返回给定日历字段的值
    • public void set(int field,int value) :将给定的日历字段设置为指定的值
    • public void add(int field,int amount):根据日历的规则,为给定的日历字段添加或者减去指定的时间量
    • public final Date getTime():将 Calendar 转成 Date 对象
    • public final void setTime(Date date):使用指定的 Date 对象重置 Calendar的时间

jdk8中新的日期、时间API

之前API面临的问题:

  1. 可变性:像日期和时间这样的类应该是不可变的。
  2. 偏移性:Date 中的年份是从 1900 开始的,而月份都从 0 开始。
  3. 格式化:格式化只对 Date 有用,Calendar 则不行。
  4. 此外,它们也不是线程安全的;不能处理闰秒等。
  • LocalDate、LocalTime、LocalDateTime –>类似于Calendar
  • Instant –>类似于Date
  • DateTimeFormatter —>类似于SimpleDateFormat

比较器(重点)

  • 自然排序
  1. 涉及到java.lang.Comparable,实现 Comparable 的类必须实现 compareTo(Object obj)方法,两个对象即通过
    compareTo(Object obj) 方法的返回值来比较大小。如果当前对象 this 大于形参对象obj,则返回正整数,如果当前对象 this 小于形参对象 obj,则返回负整数,如果当前对象 this 等于形参对象 obj,则返回零。
  2. 实现 Comparable 接口的对象列表(和数组)可以通过 Collections.sort 或Arrays.sort 进行自动排序。实现此接口的对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。
  3. Comparable 的典型实现:
  • String:按照字符串中字符的 Unicode 值进行比较

  • Character:按照字符的 Unicode 值来进行比较

  • 数值类型对应的包装类以及 BigInteger、BigDecimal:按照它们对应的数值大小进行比较

  • Boolean:true 对应的包装类实例大于 false 对应的包装类实例

  • Date、Time 等:后面的日期时间比前面的日期时间大

  • 定制排序
    场景:

  1. 当元素的类型没有实现 java.lang.Comparable 接口而又不方便修改代码(例如:一些第三方的类,你只有.class 文件,没有源文件)
  2. 如果一个类,实现了 Comparable 接口,也指定了两个对象的比较大小的规则,但是此时此刻我不想按照它预定义的方法比较大小,但是我又不能随意修改,因为会影响其他地方的使用,怎么办?

涉及到java.util.Comparator

  • 重写 compare(Object o1,Object o2)方法,比较 o1 和 o2 的大小:如果方法返回正整数,则表示 o1 大于 o2;如果返回 0,表示相等;返回负整数,表示 o1 小于 o2。
  • 可以将 Comparator 传递给 sort 方法(如 Collections.sort 或 Arrays.sort),从而允许在排序顺序上实现精确控制。

其它API

java.lang.System 类

  • System类代表系统,系统级的很多属性和控制方法都放置在该类的内部。该类位于java.lang包

  • 由于该类的构造器是private的,所以无法创建该类的对象。其内部的成员变量和成员方法都是static的,所以也可以很方便的进行调用。

  • 成员变量 Scanner scan = new Scanner(System.in);

    • System类内部包含inouterr三个成员变量,分别代表标准输入流(键盘输入),标准输出流(显示器)和标准错误输出流(显示器)。
  • 成员方法

    • native long currentTimeMillis()
      该方法的作用是返回当前的计算机时间,时间的表达格式为当前计算机时间和GMT时间(格林威治时间)1970年1月1号0时0分0秒所差的毫秒数。

    • void exit(int status)
      该方法的作用是退出程序。其中status的值为0代表正常退出,非零代表异常退出。使用该方法可以在图形界面编程中实现程序的退出功能等。

    • void gc()
      该方法的作用是请求系统进行垃圾回收。至于系统是否立刻回收,则取决于系统中垃圾回收算法的实现以及系统执行时的情况。

    • String getProperty(String key)
      该方法的作用是获得系统中属性名为key的属性对应的值。

java.lang.Runtime类

每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。

public static Runtime getRuntime(): 返回与当前 Java 应用程序相关的运行时对象。应用程序不能创建自己的 Runtime 类实例。

public long totalMemory():返回 Java 虚拟机中初始化时的内存总量。此方法返回的值可能随时间的推移而变化,这取决于主机环境。默认为物理电脑内存的1/64。

public long maxMemory():返回 Java 虚拟机中最大程度能使用的内存总量。默认为物理电脑内存的1/4。

public long freeMemory():回 Java 虚拟机中的空闲内存量。调用 gc 方法可能导致 freeMemory 返回值的增加。

和数学相关的类

  1. java.lang.Math 类包含用于执行基本数学运算的方法,如初等指数、对数、平方根和三角函数。类似这样的工具类,其所有方法均为静态方法,并且不会创建对象,调用起来非常简单。
  • public static double abs(double a) :返回 double 值的绝对值。
1
2
double d1 = Math.abs(-5); //d1的值为5
double d2 = Math.abs(5); //d2的值为5
  • public static double ceil(double a) :返回大于等于参数的最小的整数。
1
2
3
double d1 = Math.ceil(3.3); //d1的值为 4.0
double d2 = Math.ceil(-3.3); //d2的值为 -3.0
double d3 = Math.ceil(5.1); //d3的值为 6.0
  • public static double floor(double a) :返回小于等于参数最大的整数。
1
2
3
double d1 = Math.floor(3.3); //d1的值为3.0
double d2 = Math.floor(-3.3); //d2的值为-4.0
double d3 = Math.floor(5.1); //d3的值为 5.0
  • public static long round(double a) :返回最接近参数的 long。(相当于四舍五入方法)
1
2
3
4
long d1 = Math.round(5.5); //d1的值为6
long d2 = Math.round(5.4); //d2的值为5
long d3 = Math.round(-3.3); //d3的值为-3
long d4 = Math.round(-3.8); //d4的值为-4
  • public static double pow(double a,double b):返回a的b幂次方法
  • public static double sqrt(double a):返回a的平方根
  • public static double random():返回[0,1)的随机值
  • public static final double PI:返回圆周率
  • public static double max(double x, double y):返回x,y中的最大值
  • public static double min(double x, double y):返回x,y中的最小值
  • 其它:acos,asin,atan,cos,sin,tan 三角函数
1
2
3
4
double result = Math.pow(2,31);
double sqrt = Math.sqrt(256);
double rand = Math.random();
double pi = Math.PI;
  1. java.math包
    BigInteger
  • Integer类作为int的包装类,能存储的最大整型值为2^31-1,Long类也是有限的,最大为2^63-1。如果要表示再大的整数,不管是基本数据类型还是他们的包装类都无能为力,更不用说进行运算了。

  • java.math包的BigInteger可以表示不可变的任意精度的整数。BigInteger 提供所有 Java 的基本整数操作符的对应物,并提供 java.lang.Math 的所有相关方法。另外,BigInteger 还提供以下运算:模算术、GCD 计算、质数测试、素数生成、位操作以及一些其他操作。

  • 构造器

    • BigInteger(String val):根据字符串构建BigInteger对象
  • 方法

    • public BigInteger abs():返回此 BigInteger 的绝对值的 BigInteger。
    • BigInteger add(BigInteger val) :返回其值为 (this + val) 的 BigInteger
    • BigInteger subtract(BigInteger val) :返回其值为 (this - val) 的 BigInteger
    • BigInteger multiply(BigInteger val) :返回其值为 (this * val) 的 BigInteger
    • BigInteger divide(BigInteger val) :返回其值为 (this / val) 的 BigInteger。整数相除只保留整数部分。
    • BigInteger remainder(BigInteger val) :返回其值为 (this % val) 的 BigInteger。
    • BigInteger[] divideAndRemainder(BigInteger val):返回包含 (this / val) 后跟 (this % val) 的两个 BigInteger 的数组。
    • BigInteger pow(int exponent) :返回其值为 (this^exponent) 的 BigInteger。

BigDecimal

  • 一般的Float类和Double类可以用来做科学计算或工程计算,但在商业计算中,要求数字精度比较高,故用到java.math.BigDecimal类。

  • BigDecimal类支持不可变的、任意精度的有符号十进制定点数。

  • 构造器

    • public BigDecimal(double val)
    • public BigDecimal(String val) –> 推荐
  • 常用方法

    • public BigDecimal add(BigDecimal augend)
    • public BigDecimal subtract(BigDecimal subtrahend)
    • public BigDecimal multiply(BigDecimal multiplicand)
    • public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode):divisor是除数,scale指明保留几位小数,roundingMode指明舍入模式(ROUND_UP :向上加1、ROUND_DOWN :直接舍去、ROUND_HALF_UP:四舍五入)
  1. java.util.Random
    用于产生随机数
  • boolean nextBoolean():返回下一个伪随机数,它是取自此随机数生成器序列的均匀分布的 boolean 值。

  • void nextBytes(byte[] bytes):生成随机字节并将其置于用户提供的 byte 数组中。

  • double nextDouble():返回下一个伪随机数,它是取自此随机数生成器序列的、在 0.0 和 1.0 之间均匀分布的 double 值。

  • float nextFloat():返回下一个伪随机数,它是取自此随机数生成器序列的、在 0.0 和 1.0 之间均匀分布的 float 值。

  • double nextGaussian():返回下一个伪随机数,它是取自此随机数生成器序列的、呈高斯(“正态”)分布的 double 值,其平均值是 0.0,标准差是 1.0。

  • int nextInt():返回下一个伪随机数,它是此随机数生成器的序列中均匀分布的 int 值。

  • int nextInt(int n):返回一个伪随机数,它是取自此随机数生成器序列的、在 0(包括)和指定值(不包括)之间均匀分布的 int 值。

  • long nextLong():返回下一个伪随机数,它是取自此随机数生成器序列的均匀分布的 long 值。

企业真题(十一)

2.1 String

1. 以下两种方式创建的String对象有什么不同?(*团)

1
2
3
String str = new String("test");

String str = "test";

2. String s = new String(“xyz”);创建了几个String Object? (新*陆)

两个

3. String a=”abc” String b=”a”+”bc” 问a==b?(网*邮箱)

是!

4. String 中 “+” 怎样实现?(阿*)

常量 + 常量 :略

变量 + 常量 、变量+变量:创建一个StringBuilder的实例,通过append()添加字符串,最后调用toString()返回一个字符串。(toString()内部new 一个String的实例)

5. Java中String是不是final的?(凡*科技)

1
2
3
4
类似问题:
> String被哪些类继承?(网*邮箱)
> 是否可以继承String类?(湖南*利软件)
> String 是否可以继承?(阿*)

6. String为啥不可变,在内存中的具体形态?(阿*)

规定不可变。

String:提供字符串常量池。

7. String 可以在 switch中使用吗?(上海*睿)

可以。从jdk7开始可以使用

8. String中有哪些方法?列举几个(闪*购)

。。。

9. subString()到底做了什么?(银*数据)

String str = “hello”;

String subStr = str.subString(1,3); //底层是new的方式返回一个subStr,实体内容是”el”

2.2 String、StringBuffer、StringBuilder

1. Java中操作字符串有哪些类?他们之间有什么区别。(南*电网)

1
2
3
4
类似问题:
> String 和 StringBuffer区别?(亿*国际、天*隆、*团)
> StringBuilder和StrignBuffer的区别?(平*金服)
> StringBuilder和StringBuffer的区别以及实现?(*为)
1
2
3
> String:不可变的字符序列;底层使用char[] (jdk8及之前),底层使用byte[] (jdk9及之后)
> StringBuffer:可变的字符序列;JDK1.0声明,线程安全的,效率低;底层使用char[] (jdk8及之前),底层使用byte[] (jdk9及之后)
> StringBuilder:可变的字符序列;JDK5.0声明,线程不安全的,效率高;底层使用char[] (jdk8及之前),底层使用byte[] (jdk9及之后)

2. String的线程安全问题(闪*购)

线程不安全的

3. StringBuilder和StringBuffer的线程安全问题(润*软件)

2.3 Comparator与Comparable

1. 简单说说 Comparable 和 Comparator 的区别和场景?(软**力)

2. Comparable 接口和 Comparator 接口实现比较(阿*)

集合框架

数组存储数据方面的特点和弊端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
数组存储多个数据方面的特点:
> 数组一旦初始化,其长度就是确定的。
> 数组中的多个元素是依次紧密排列的,有序的,可重复的
> (优点) 数组一旦初始化完成,其元素的类型就是确定的。不是此类型的元素,就不能添加到此数组中。
int[] arr = new int[10];
arr[0] = 1;
arr[1] = "AA";//编译报错

Object[] arr1 = new Object[10];
arr1[0] = new String();
arr1[1] = new Date();
> (优点)元素的类型既可以是基本数据类型,也可以是引用数据类型。

数组存储多个数据方面的弊端:
> 数组一旦初始化,其长度就不可变了。
> 数组中存储数据特点的单一性。对于无序的、不可重复的场景的多个数据就无能为力了。
> 数组中可用的方法、属性都极少。具体的需求,都需要自己来组织相关的代码逻辑。
> 针对于数组中元素的删除、插入操作,性能较差。

Java集合框架体系(java.util包下)

1
2
3
4
5
6
7
8
9
10
java.util.Collection:存储一个一个的数据
|-----子接口:List:存储有序的、可重复的数据 ("动态"数组)
|---- ArrayList(主要实现类)、LinkedList、Vector

|-----子接口:Set:存储无序的、不可重复的数据(高中学习的集合)
|---- HashSet(主要实现类)、LinkedHashSet、TreeSet


java.util.Map:存储一对一对的数据(key-value键值对,(x1,y1)、(x2,y2) --> y=f(x),类似于高中的函数)
|---- HashMap(主要实现类)、LinkedHashMap、TreeMap、Hashtable、Properties
1
2
3
4
5
学习的程度把握:
层次1:针对于具体特点的多个数据,知道选择相应的适合的接口的主要实现类,会实例化,会调用常用的方法。
层次2:区分接口中不同的实现类的区别。
*****************
层次3:① 针对于常用的实现类,需要熟悉底层的源码 ② 熟悉常见的数据结构 (第14章讲)

Collection的常用方法

常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
add(Object obj)
addAll(Collection coll)
clear()
isEmpty()
size()
contains(Object obj)
containsAll(Collection coll)
retainAll(Collection coll)
remove(Object obj)
removeAll(Collection coll)
hashCode()
equals()
toArray()
**************
iterator() ---> 引出了迭代器接口
1
2
向Collection中添加元素的要求:
> 要求元素所属的类一定要重写equals()!
1
2
3
集合与数组的相互转换:
集合 ---> 数组:toArray()
数组 ---> 集合:调用Arrays的静态方法asList(Object ... objs),返回一个List

迭代器接口

  • 设计模式的一种
  • 迭代器不负责数据的存储;负责对集合类的遍历
1
2
3
4
5
6
7
1. 如何获取迭代器(Iterator)对象?
Iterator iterator = coll.iterator();

2. 如何实现遍历(代码实现)
while(iterator.hasNext()){
System.out.println(iterator.next()); //next():①指针下移 ② 将下移以后集合位置上的元素返回
}
  • 增强for循环(foreach循环)的使用(jdk5.0新特性)
    1
    2
    3
    for(要遍历的集合或数组元素的类型 临时变量 : 要遍历的集合或数组变量){
    操作临时变量的输出
    }

    针对于集合来讲,增强for循环的底层仍然使用的是迭代器。
    增强for循环的执行过程中,是将集合或数组中的元素依次赋值给临时变量,注意,循环体中对临时变量的修改,可能不会导致原有集合或数组中元素的修改。

Collection的子接口:List

List接口中存储数据的特点:用于存储有序的、可以重复的数据。—> 使用List替代数组,”动态”数组

  • 常用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
小结:

add(Object obj)
addAll(Collection coll)

remove(Object obj)
remove(int index)

set(int index, Object ele)

get(int index)

add(int index, Object ele)
addAll(int index, Collection eles)
长度
size()
遍历
iterator() :使用迭代器进行遍历
增强for循环
一般的for循环
1
2
3
4
5
6
7
8
9
List及其实现类特点
java.util.Collection:存储一个一个的数据
|-----子接口:List:存储有序的、可重复的数据 ("动态"数组)
|---- ArrayList:List的主要实现类;线程不安全的、效率高;底层使用Object[]数组存储
在添加数据、查找数据时,效率较高;在插入、删除数据时,效率较低
|---- LinkedList:底层使用双向链表的方式进行存储;在对集合中的数据进行频繁的删除、插入操作时,建议 使用此类在插入、删除数据时,效率较高;在添加数据、查找数据时,效率较低;
|---- Vector:List的古老实现类;线程安全的、效率低;底层使用Object[]数组存储

[面试题] ArrayList、Vector的区别? ArrayList、LinkedList的区别?

Collection的子接口:Set

  • Set中的常用的方法都是Collection中声明的方法,没有新增的方法
  • 常见的实现类的对比
1
2
3
4
5
6
java.util.Collection:存储一个一个的数据
|-----子接口:Set:存储无序的、不可重复的数据(高中学习的集合)
|---- HashSet:主要实现类;底层使用的是HashMap,即使用数组+单向链表+红黑树结构进行存储。(jdk8中)
|---- LinkedHashSet:是HashSet的子类;在现有的数组+单向链表+红黑树结构的基础上,又添加了
一组双向链表,用于记录添加元素的先后顺序。即:我们可以按照添加元素的顺序实现遍历。便于频繁的查询操作。
|---- TreeSet:底层使用红黑树存储。可以按照添加的元素的指定的属性的大小顺序进行遍历。
  • 开发中的使用频率及场景:

    较List、Map来说,Set使用的频率比较少。
    用来过滤重复数据

  • 难点: Set中无序性、不可重复性的理解(以HashSet及其子类为例说明)

1
2
3
4
5
6
7
8
>无序性: != 随机性。
添加元素的顺序和遍历元素的顺序不一致,是不是就是无序性呢? No!
到底什么是无序性?与添加的元素的位置有关,不像ArrayList一样是依次紧密排列的。
这里是根据添加的元素的哈希值,计算的其在数组中的存储位置。此位置不是依次排列的,表现为无序性。

>不可重复性:添加到Set中的元素是不能相同的。
比较的标准,需要判断hashCode()得到的哈希值以及equals()得到的boolean型的结果。
哈希值相同且equals()返回true,则认为元素是相同的。
1
2
3
添加到HashSet/LinkedHashSet中元素的要求:
>要求元素所在的类要重写两个方法:equals() 和 hashCode()。
>同时,要求equals() 和 hashCode()要保持一致性!我们只需要在IDEA中自动生成两个方法的重写即可,即能保证两个方法的一致性。
  • TreeSet的使用
    1. 底层的数据结构:红黑树
    2. 添加数据后的特点:可以按照添加的元素的指定的属性的大小顺序进行遍历。
    3. 向TreeSet中添加的元素的要求:

      要求添加到TreeSet中的元素必须是同一个类型的对象,否则会ClassCastException.
      添加的元素需要考虑排序:① 自然排序 ② 定制排序

    4. 判断数据是否相同的标准

      不再是考虑hashCode()和equals()方法了,也就意味着添加到TreeSet中的元素所在的类不需要重写hashCode()和equals()方法了
      比较元素大小的或比较元素是否相等的标准就是考虑自然排序或定制排序中,compareTo()或compare()的返回值。如果compareTo()或compare()的返回值为0,则认为两个对象是相等的。由于TreeSet中不能存放相同的元素,则后一个相等的元素就不能添加到TreeSet中。

Map接口

  • 常用的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
增:
put(Object key,Object value)
putAll(Map m)
删:
Object remove(Object key)
改:
put(Object key,Object value)
putAll(Map m)
查:
Object get(Object key)
长度:
size()
遍历:
遍历key集:Set keySet()
遍历value集:Collection values()
遍历entry集:Set entrySet()
  • 常用的实现类
1
2
3
4
5
6
7
8
9
java.util.Map:存储一对一对的数据(key-value键值对,(x1,y1)、(x2,y2) --> y=f(x),类似于高中的函数)
|---- HashMap:主要实现类;线程不安全的,效率高;可以添加null的key和value值;底层使用数组+单向链表+红黑树结构存储(jdk8)
|---- LinkedHashMap:是HashMap的子类;在HashMap使用的数据结构的基础上,增加了一对双向链表,用于记录添加的元素的先后顺序,进而我们在遍历元素时,就可以按照添加的顺序显示。开发中,对于频繁的遍历操作,建议使用此类。
|---- TreeMap:底层使用红黑树存储;可以按照添加的key-value中的key元素的指定的属性的大小顺序进行遍历。需要考虑使用①自然排序 ②定制排序。
|---- Hashtable:古老实现类;线程安全的,效率低;不可以添加null的key或value值;底层使用数组+单向链表结构存储(jdk8)
|---- Properties:其key和value都是String类型。常用来处理属性文件。


[面试题] 区别HashMap和Hashtable、区别HashMap和LinkedHashMap、HashMap的底层实现(① new HashMap() ② put(key,value))
1
2
3
4
5
HashMap中元素的特点:
> HashMap中的所有的key彼此之间是不可重复的、无序的。所有的key就构成一个Set集合。--->key所在的类要重写hashCode()和equals()
> HashMap中的所有的value彼此之间是可重复的、无序的。所有的value就构成一个Collection集合。--->value所在的类要重写equals()
> HashMap中的一个key-value,就构成了一个entry。
> HashMap中的所有的entry彼此之间是不可重复的、无序的。所有的entry就构成了一个Set集合。
  • (了解)TreeMap的使用

    底层使用红黑树存储;
    可以按照添加的key-value中的key元素的指定的属性的大小顺序进行遍历。
    需要考虑使用①自然排序 ②定制排序。
    要求:向TreeMap中添加的key必须是同一个类型的对象。

  • (重要)Properties的使用
    Properties:是Hashtable的子类,其key和value都是String类型的,常用来处理属性文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    public class PropertiesTest {
    @Test
    public void test() throws IOException { //注意:因为设计到流的操作,为了确保流能关闭,建议使用try-catch-finally
    //方式1:数据和代码耦合度高;如果修改的话,需要重写的编译代码、打包发布,繁琐
    //数据
    // String name = "Tom";
    // String password = "abc123";

    //代码:用于操作name,password
    //...

    //方式2:将数据封装到具体的配置文件中,在程序中读取配置文件中的信息。实现了
    //数据和代码的解耦;由于我们没有修改代码,就省去了重新编译和打包的过程。
    File file = new File("info.properties"); //注意,要提前创建好
    // System.out.println(file.getAbsolutePath());
    FileInputStream fis = new FileInputStream(file);

    Properties pros = new Properties();
    pros.load(fis); //加载流中的文件中的数据

    //读取数据
    String name = pros.getProperty("name");
    String pwd = pros.getProperty("password");

    System.out.println(name + ":" + pwd);

    fis.close();
    }

    }

Collections工具类的使用

1
2
3
区分Collection 和 Collections
Collection:集合框架中的用于存储一个一个元素的接口,又分为List和Set等子接口。
Collections:用于操作集合框架的一个工具类。此时的集合框架包括:Set、List、Map
  • Collections中的常用方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    排序操作:
    - reverse(List):反转 List 中元素的顺序
    - shuffle(List):对 List 集合元素进行随机排序
    - sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
    - sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
    - swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换

    查找
    - Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
    - Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
    - Object min(Collection):根据元素的自然顺序,返回给定集合中的最小元素
    - Object min(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最小元素
    - int binarySearch(List list,T key)在List集合中查找某个元素的下标,但是List的元素必须是T或T的子类对象,而且必须是可比较大小的,即支持自然排序的。而且集合也事先必须是有序的,否则结果不确定。
    - int binarySearch(List list,T key,Comparator c)在List集合中查找某个元素的下标,但是List的元素必须是T或T的子类对象,而且集合也事先必须是按照c比较器规则进行排序过的,否则结果不确定。
    - int frequency(Collection c,Object o):返回指定集合中指定元素的出现次数

    复制、替换
    - void copy(List dest,List src):将src中的内容复制到dest中
    - boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值
    - 提供了多个unmodifiableXxx()方法,该方法返回指定 Xxx的不可修改的视图。

    添加
    - boolean addAll(Collection c,T... elements)将所有指定元素添加到指定 collection 中。

    同步
    - Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题

企业真题(十二)

2.1 集合概述

1. List,Set,Map是否继承自collection接口?(北京中*译咨询、思*贸易)

Map不是。

2. 说说List,Set,Map三者的区别(民*银行)

1
2
类似问题:
> Map与Set、List的区别(纬*)

3. 写出list、map、set接口的实现类,并说出其特点(华**为)

1
2
3
类似问题:
> 集合有哪些, 各自有哪些特点, 各自的API有哪些?(湖**利软件)
> List Map Set三个接口在存储元素时个有什么特点(*软)

4. 常见集合类的区别和适用场景(饿**)

5. 集合的父类是谁?哪些安全的?(北京中**信)

略。 不安全:ArrayList、HashMap、HashSet ; 安全:Vector、Hashtable

6. 集合说一下哪些是线程不安全的(*科软)

7. 遍历集合的方式有哪些?(恒*电子)

  • 迭代器Iterator用来遍历Collection,不能用来遍历Map!

  • 增强for

  • 一般的for:可以用来遍历List

2.2 List接口

1. List下面有哪些实现(软**力)

2. ArrayList与LinkedList区别?(O**O、滴*、汇*天下、拓*软件、博纳**软件、上海*进天下,北京永生**信息、*联、在*途游)

1
2
类似问题:
> ArrayList跟LinkedList的区别详细说出?(阿*校招、*东)

略。 补充上第14章中的源码(底层的数据结构)

3. ArrayList与Vector区别呢?为什么要用ArrayList取代Vector呢?(湖**利软件)

Vector效率低。

4. Java.util.ArrayList常用的方法有哪些?(华**为)

5. Arraylist 是有序还是无序?为什么?(蜜*信息)

有序;底层使用数组:Object[]

2.3 Set接口

1. Set集合有哪些实现类,分别有什么特点?(拓*软件)

1
2
类似问题:
> Set的实现类有哪些?(博*科技)

2. List集合和Set集合的区别?(亚*科技、*海*翼科技,*华电*系统,达*贷)

3. Set里的元素是不能重复的,那么用什么方法来区分重复与否呢? 是用==还是equals()? 它们有何区别?(鸿*网络)

1
2
3
类似问题:
> 1.HashSet如何检查重复(创*科技)
> 3.Set使用哪个区分不能重复的元素的?(北京创**荣信息)

hashCode() 、 equals()

4. TreeSet两种排序方式在使用的时候怎么起作用?(拓*软件)

在添加新的元素时,需要调用compareTo() 或 compare()

5. TreeSet的数据结构(*米)

红黑树

2.4 Map接口

1. 说一下Java的集合Map有哪些Map?(奥*医药)

2. final怎么用,修饰Map可以继续添加数据吗?(*深蓝)

final HashMap map = new HashMap();

map.put(“AA”,123);

可以!

3. Set和Map的比较(亚*科技)

HashSet底层就是HashMap

LinkedHashSet底层就是LinkedHashMap

TreeSet底层就是TreeMap

4. HashMap说一下,线程安全吗?(*米)

1
2
3
类似问题:
> HashMap为什么线程不安全?(微*银行)
> HashMap是线程安全的吗?为什么不安全?(*团、*东、顺*)

不安全

5. HashMap和Hashbable的区别?(银*数据、阿**巴芝麻信用、*众银行、爱*信、杭州*智公司)

1
2
类似问题:
> HashMap 和 HashTable 有什么区别,以及如何使用,以及他的一些方法?(阿*校招、*东、*度校招、顺*)

6. Hashtable是怎么实现的,为什么线程安全?(迪*创新)

数组+单向链表;底层方法使用synchronized修饰

7. HashMap和LinkedHashMap的区别(北京*晨阳光)

略。

8. HashMap 和 TreeMap 的区别(*度,太极**、*线途游、阿*校招)

底层的数据结构截然不同。

9. HashMap里面实际装的是什么?(惠*)

JDK7:HashMap内部声明了Entry,实现了Map中的Entry接口。(key,value作为Entry的两个属性出现)

JDK8:HashMap内部声明了Node,实现了Map中的Entry接口。(key,value作为Node的两个属性出现)

10. HashMap的key存储在哪里?和value存储在一起吗?那么value存储在哪里?说具体点?(湖**利软件、天*伟业)

数组+链表+红黑树。 key、value作为Node的属性出现

11. 自定义类型可以作为Key么?(阿*)

可以! 要重写hashCode() 和equals()

Collections

1. 集合类的工具类是谁?用过工具类哪些方法?(顺*)

Collections。略

2. Collection 和 Collections的区别?(平*金服、*软)

3. ArrayList 如何实现排序(阿*)

Collections.sort(list) 或 Collections.sort(list,comparator)

4. HashMap是否线程安全,怎样解决HashMap的线程不安全(中*卫星)

1
2
类似问题:
> 怎么实现HashMap线程安全?(*团、*东、顺*)

泛型

泛型的理解

  • ,泛型参数,使用引用数据类型来赋值。

泛型在集合、比较器中的使用(重点)

  • 在集合中使用泛型之前可能存在的问题
    问题1:类型不安全。因为add()的参数是Object类型,意味着任何类型的对象都可以添加成功
    问题2:需要使用强转操作,繁琐。还有可能导致ClassCastException异常。
  • 集合:ArrayList、HashMap、Iterator
  • 比较器:Comparable、Comparator

自定义泛型类/泛型接口、泛型方法(熟悉)

  • class Order{ }
  • public 返回值类型 方法名(形参列表){}
  • 具体的细节,见IDEA中的笔记。

泛型在继承上的体现

1
2
3
4
5
6
7
8
1. 类SuperA是类A的父类,则G<SuperA> 与 G<A>的关系:G<SuperA> 和 G<A>是并列的两个类,没有任何子父类的关系。

比如:ArrayList<Object> 、ArrayList<String>没有关系

2. 类SuperA是类A的父类或接口,SuperA<G> 与 A<G>的关系:SuperA<G> 与A<G> 有继承或实现的关系。
即A<G>的实例可以赋值给SuperA<G>类型的引用(或变量)

比如:List<String> 与 ArrayList<String>

通配符的使用

  • ? 的使用 (重点)
    • 以集合为例:可以读取数据、不能写入数据(例外:null)
  • ? extends A
    • 以集合为例:可以读取数据、不能写入数据(例外:null)
  • ? super A
    • 以集合为例:可以读取数据、可以写入A类型或A类型子类的数据(例外:null)

企业真题(十三)

1. Java 的泛型是什么?有什么好处和优点?JDK 不同版本的泛型有什么区别?(软*动力)

泛型,是程序中出现的不确定的类型。

以集合来举例:把一个集合中的内容限制为一个特定的数据类型,这就是generic背后的核心思想。

jdk7.0新特性:

1
ArrayList<String> list = new ArrayList<>(); //类型推断

后续版本的新特性:

1
Comparator<Employee> comparator = new Comparator<>(){} //类型推断

2. 说说你对泛型的了解(*软国际)

数据结构与集合源码

数据结构

  • 概念:数据结构,就是一种程序设计优化的方法论,研究数据的逻辑结构物理结构以及它们之间相互关系,并对这种结构定义相应的运算,目的是加快程序的执行速度、减少内存占用的空间。

  • 数据结构的研究对象:

    • ① 数据间的逻辑关系(集合关系、一对一、一对多、多对多)
    • ② 数据的存储结构(或物理结构)
      • 角度一:顺序结构、链式结构、索引结构、哈希结构
      • 角度二:线性表(一维数组、链表、栈、队列)、树(二叉树、B+树)、图(多对多)、哈希表(HashMap、HashSet)
    • ③ 相关运算

链表

  • 逻辑结构:线性结构

  • 物理结构:不要求连续的存储空间

  • 存储特点:链表由一系列结点node(链表中每一个元素称为结点)组成,结点可以在代码执行过程中动态创建。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域

image-20220511113744772

  • 常见的链表结构有如下的形式:

1563448858180

  • 栈(Stack)又称为堆栈或堆叠,是限制仅在表的一端进行插入和删除运算的线性表。

  • 栈按照先进后出(FILO,first in last out)的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶。每次删除(退栈)的总是删除当前栈中最后插入(进栈)的元素,而最先插入的是被放在栈的底部,要到最后才能删除。

  • 核心类库中的栈结构有Stack和LinkedList。

    • Stack就是顺序栈,它是Vector的子类。
    • LinkedList是链式栈。
  • 体现栈结构的操作方法:

    • peek()方法:查看栈顶元素,不弹出
    • pop()方法:弹出栈
    • push(E e)方法:压入栈
  • 时间复杂度:

    • 索引: O(n)
    • 搜索: O(n)
    • 插入: O(1)
    • 移除: O(1)
  • 图示:

队列

  • 队列(Queue)是只允许在一端进行插入,而在另一端进行删除的运算受限的线性表。

  • 队列是逻辑结构,其物理结构可以是数组,也可以是链表。

  • 队列的修改原则:队列的修改是依先进先出(FIFO)的原则进行的。新来的成员总是加入队尾(即不允许”加塞”),每次离开的成员总是队列头上的(不允许中途离队),即当前”最老的”成员离队。

  • 图示:

image-20220826010241172

树与二叉树

  1. 树的理解

专有名词解释:

结点:树中的数据元素都称之为结点

根节点:最上面的结点称之为根,一颗树只有一个根且由根发展而来,从另外一个角度来说,每个结点都可以认为是其子树的根

父节点:结点的上层结点,如图中,结点K的父节点是E、结点L的父节点是G

子节点:节点的下层结点,如图中,节点E的子节点是K节点、节点G的子节点是L节点

兄弟节点:具有相同父节点的结点称为兄弟节点,图中F、G、H互为兄弟节点

结点的度数:每个结点所拥有的子树的个数称之为结点的度,如结点B的度为3

树叶:度数为0的结点,也叫作终端结点,图中D、K、F、L、H、I、J都是树叶

非终端节点(或分支节点):树叶以外的节点,或度数不为0的节点。图中根、A、B、C、E、G都是

树的深度(或高度):树中结点的最大层次数,图中树的深度为4

结点的层数:从根节点到树中某结点所经路径上的分支树称为该结点的层数,根节点的层数规定为1,其余结点的层数等于其父亲结点的层数+1

同代:在同一棵树中具有相同层数的节点

  1. 二叉树的基本概念

二叉树(Binary tree)是树形结构的一个重要类型。二叉树特点是每个结点最多只能有两棵子树,且有左右之分。许多实际问题抽象出来的数据结构往往是二叉树形式,二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。

1563449427345

  1. 二叉树的遍历
  • 前序遍历:中左右(根左右)

    即先访问根结点,再前序遍历左子树,最后再前序遍历右子 树。前序遍历运算访问二叉树各结点是以根、左、右的顺序进行访问的。

  • 中序遍历:左中右(左根右)

    即先中前序遍历左子树,然后再访问根结点,最后再中序遍 历右子树。中序遍历运算访问二叉树各结点是以左、根、右的顺序进行访问的。

  • 后序遍历:左右中(左右根)

    即先后序遍历左子树,然后再后序遍历右子树,最后访问根 结点。后序遍历运算访问二叉树各结点是以左、右、根的顺序进行访问的。

前序遍历:ABDHIECFG

中序遍历:HDIBEAFCG

后序遍历:HIDEBFGCA

  1. 经典二叉树

1、满二叉树: 除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树。 第n层的结点数是2的n-1次方,总的结点个数是2的n次方-1

1574575163883

2、完全二叉树: 叶结点只能出现在最底层的两层,且最底层叶结点均处于次底层叶结点的左侧。

1574575180247

3、二叉排序/查找/搜索树:即为BST (binary search/sort tree)。满足如下性质:
(1)若它的左子树不为空,则左子树上所有结点的值均小于它的根节点的值;
(2)若它的右子树上所有结点的值均大于它的根节点的值;
(3)它的左、右子树也分别为二叉排序/查找/搜索树。

对二叉查找树进行中序遍历,得到有序集合。便于检索。

4、平衡二叉树:(Self-balancing binary search tree,AVL)首先是二叉排序树,此外具有以下性质:
(1)它是一棵空树或它的左右两个子树的高度差的绝对值不超过1
(2)并且左右两个子树也都是一棵平衡二叉树
(3)不要求非叶节点都有两个子结点

平衡二叉树的目的是为了减少二叉查找树的层次,提高查找速度。平衡二叉树的常用实现有红黑树、AVL、替罪羊树、Treap、伸展树等。

5、红黑树:即Red-Black Tree。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。

红黑树是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,它是在 1972 年由 Rudolf Bayer 发明的。红黑树是复杂的,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的:它可以在 O(log n)时间内做查找,插入和删除, 这里的 n 是树中元素的数目。

红黑树的特性:

  • 每个节点是红色或者黑色

  • 根节点是黑色

  • 每个叶子节点(NIL)是黑色。(注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点)

  • 每个红色节点的两个子节点都是黑色的。(从每个叶子到根的所有路径上不能有两个连续的红色节点)

  • 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点(确保没有一条路径会比其他路径长出2倍)

当我们插入或删除节点时,可能会破坏已有的红黑树,使得它不满足以上5个要求,那么此时就需要进行处理,使得它继续满足以上的5个要求:

1、recolor :将某个节点变红或变黑

2、rotation :将红黑树某些结点分支进行旋转(左旋或右旋)

image-20221208212053079

红黑树可以通过红色节点和黑色节点尽可能的保证二叉树的平衡。主要是用它来存储有序的数据,它的时间复杂度是O(logN),效率非常之高。

List接口下的实现类的源码剖析

【面试题】ArrayList、Vector、LinkedList的三者的对比?

  • 层次1:
1
2
3
4
5
|-----子接口:List:存储有序的、可重复的数据 ("动态"数组)
|---- ArrayList:List的主要实现类;线程不安全的、效率高;底层使用Object[]数组存储
在添加数据、查找数据时,效率较高;在插入、删除数据时,效率较低
|---- LinkedList:底层使用双向链表的方式进行存储;在对集合中的数据进行频繁的删除、插入操作时,建议使用此类在插入、删除数据时,效率较高;在添加数据、查找数据时,效率较低;
|---- Vector:List的古老实现类;线程安全的、效率低;底层使用Object[]数组存储
  • 层次2:查看相关api的源码

ArrayList源码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1. jdk7版本:(以jdk1.7.0_07为例)
//如下代码的执行:底层会初始化数组,数组的长度为10。Object[] elementData = new Object[10];
ArrayList<String> list = new ArrayList<>();

list.add("AA"); //elementData[0] = "AA";
list.add("BB");//elementData[1] = "BB";
...
当要添加第11个元素的时候,底层的elementData数组已满,则需要扩容。默认扩容为原来长度的1.5倍。并将原有数组
中的元素复制到新的数组中。


2. jdk8版本:(以jdk1.8.0_271为例)
//如下代码的执行:底层会初始化数组,即:Object[] elementData = new Object[]{};
ArrayList<String> list = new ArrayList<>();

list.add("AA"); //首次添加元素时,会初始化数组elementData = new Object[10];elementData[0] = "AA";
list.add("BB");//elementData[1] = "BB";
...
当要添加第11个元素的时候,底层的elementData数组已满,则需要扩容。默认扩容为原来长度的1.5倍。并将原有数组
中的元素复制到新的数组中。

小结:
jdk1.7.0_07版本中:ArrayList类似于饿汉式
jdk1.8.0_271版本中:ArrayList类似于懒汉式

Vector源码解析

1
2
3
4
5
6
7
Vector源码解析:(以jdk1.8.0_271为例)

Vector v = new Vector(); //底层初始化数组,长度为10.Object[] elementData = new Object[10];
v.add("AA"); //elementData[0] = "AA";
v.add("BB");//elementData[1] = "BB";
...
当添加第11个元素时,需要扩容。默认扩容为原来的2倍。

LinkedList源码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
LinkedList在jdk8中的源码解析:

LinkedList<String> list = new LinkedList<>(); //底层也没做啥
list.add("AA"); //将"AA"封装到一个Node对象1中,list对象的属性first、last都指向此Node对象1。
list.add("BB"); //将"BB"封装到一个Node对象2中,对象1和对象2构成一个双向链表,同时last指向此Node对象2

...
因为LinkedList使用的是双向链表,不需要考虑扩容问题。

LinkedList内部声明:
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}

  • 启示与开发建议
  1. Vector基本不使用了。

  2. ArrayList底层使用数组结构,查找和添加(尾部添加)操作效率高,时间复杂度为O(1);删除和插入操作效率低,时间复杂度为O(n)
    LinkedList底层使用双向链表结构,删除和插入操作效率高,时间复杂度为O(1);查找和添加(尾部添加)操作效率高,时间复杂度为O(n) (有可能添加操作是O(1))

  3. 在选择了ArrayList的前提下,new ArrayList() : 底层创建长度为10的数组。new ArrayList(int capacity):底层创建指定capacity长度的数组。如果开发中,大体确认数组的长度,则推荐使用ArrayList(int capacity)这个构造器,避免了底层的扩容、复制数组的操作。

Map接口下的实现类的源码剖析

HashMap源码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
1. jdk7中创建对象和添加数据过程(以JDK1.7.0_07为例说明):

//创建对象的过程中,底层会初始化数组Entry[] table = new Entry[16];
HashMap<String,Integer> map = new HashMap<>();
...
map.put("AA",78); //"AA"和78封装到一个Entry对象中,考虑将此对象添加到table数组中。
...

添加/修改的过程:
将(key1,value1)添加到当前的map中:
首先,需要调用key1所在类的hashCode()方法,计算key1对应的哈希值1,此哈希值1经过某种算法(hash())之后,得到哈希值2。
哈希值2再经过某种算法(indexFor())之后,就确定了(key1,value1)在数组table中的索引位置i。
1.1 如果此索引位置i的数组上没有元素,则(key1,value1)添加成功。 ---->情况1
1.2 如果此索引位置i的数组上有元素(key2,value2),则需要继续比较key1和key2的哈希值2 --->哈希冲突
2.1 如果key1的哈希值2与key2的哈希值2不相同,则(key1,value1)添加成功。 ---->情况2
2.2 如果key1的哈希值2与key2的哈希值2相同,则需要继续比较key1和key2的equals()。要调用key1所在类的equals(),将key2作为参数传递进去。
3.1 调用equals(),返回false: 则(key1,value1)添加成功。 ---->情况3
3.2 调用equals(),返回true: 则认为key1和key2是相同的。默认情况下,value1替换原有的value2。

说明:情况1:将(key1,value1)存放到数组的索引i的位置
情况2,情况3:(key1,value1)元素与现有的(key2,value2)构成单向链表结构,(key1,value1)指向(key2,value2)

随着不断的添加元素,在满足如下的条件的情况下,会考虑扩容:
(size >= threshold) && (null != table[i])
当元素的个数达到临界值(-> 数组的长度 * 加载因子)时,就考虑扩容。默认的临界值 = 16 * 0.75 --> 12.
默认扩容为原来的2倍。


2. jdk8与jdk7的不同之处(以jdk1.8.0_271为例):
① 在jdk8中,当我们创建了HashMap实例以后,底层并没有初始化table数组。当首次添加(key,value)时,进行判断,如果发现table尚未初始化,则对数组进行初始化。
② 在jdk8中,HashMap底层定义了Node内部类,替换jdk7中的Entry内部类。意味着,我们创建的数组是Node[]
③ 在jdk8中,如果当前的(key,value)经过一系列判断之后,可以添加到当前的数组角标i中。如果此时角标i位置上有
元素。在jdk7中是将新的(key,value)指向已有的旧的元素(头插法),而在jdk8中是旧的元素指向新的
(key,value)元素(尾插法)。 "七上八下"
④ jdk7:数组+单向链表
jk8:数组+单向链表 + 红黑树
什么时候会使用单向链表变为红黑树:如果数组索引i位置上的元素的个数达到8,并且数组的长度达到64时,我们就将此索引i位置上
的多个元素改为使用红黑树的结构进行存储。(为什么修改呢?红黑树进行put()/get()/remove()
操作的时间复杂度为O(logn),比单向链表的时间复杂度O(n)的好。性能更高。
什么时候会使用红黑树变为单向链表:当使用红黑树的索引i位置上的元素的个数低于6的时候,就会将红黑树结构退化为单向链表。


3. 属性/字段:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认的初始容量 16
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量 1 << 30
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认加载因子
static final int TREEIFY_THRESHOLD = 8; //默认树化阈值8,当链表的长度达到这个值后,要考虑树化
static final int UNTREEIFY_THRESHOLD = 6;//默认反树化阈值6,当树中结点的个数达到此阈值后,要考虑变为链表

//当单个的链表的结点个数达到8,并且table的长度达到64,才会树化。
//当单个的链表的结点个数达到8,但是table的长度未达到64,会先扩容
static final int MIN_TREEIFY_CAPACITY = 64; //最小树化容量64

transient Node<K,V>[] table; //数组
transient int size; //记录有效映射关系的对数,也是Entry对象的个数
int threshold; //阈值,当size达到阈值时,考虑扩容
final float loadFactor; //加载因子,影响扩容的频率

LinkedHashMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1. LinkedHashMap 与 HashMap 的关系:
> LinkedHashMap 是 HashMap的子类。
> LinkedHashMap在HashMap使用的数组+单向链表+红黑树的基础上,又增加了一对双向链表,记录添加的(key,value)的
先后顺序。便于我们遍历所有的key-value。

LinkedHashMap重写了HashMap的如下方法:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}


2. 底层结构:LinkedHashMap内部定义了一个Entry
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after; //增加的一对双向链表
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}

HashSet和LinkedHashSet的源码分析

HashSet底层使用的是HashMap
LinkedHashSet底层使用的是LinkedHashMap

企业真题(十四)

数据结构相关

1. 链表和数组有什么区别?(腾*)

2. 栈是如何运行的?(西*信息技术)

先进后出。属于ADT(abstract data type),可以使用数组、链表实现栈结构

List集合源码相关

1. ArrayList的默认大小是多少,以及扩容机制(顺*、凡*科技)

1
2
3
4
类似问题:
> 说说ArrayList的扩容机制吧(国*电网)
> 讲一下ArrayList的扩容机制(*实在)
> ArrayList的扩容机制,为什么是10,为什么是1.5倍(*软国际)

2. ArrayList的底层是怎么实现的?(腾*)

1
2
3
类似问题:
集合类的ArrayList底层(安全不安全,扩容,初始大小,添加删除查询是怎么操作的,底层是什么组成的)
(湖**利软件、汇*云通、猎*、苏州***动、上海*进天下、北京博*软件、*科软、大连*点科技、中*亿达、德*物流、天*伟业、猫*娱乐)

略。

建议:ArrayList(int capacity){}

3. 在ArrayList中remove后面几个元素该怎么做?(惠*、中*亿达)

前移。

4. ArrayList1.7和1.8的区别(拓*思)

类似于饿汉式、懒汉式

5. 数组和 ArrayList 的区别(阿*、*科软)

ArrayList看做是对数组的常见操作的封装。

6. 什么是线程安全的List?(平*金服)

Vector:线程安全的。

ArrayList:线程不安全。—-> 使用同步机制处理。

1
2
HashMap:线程不安全。 ----> 使用同步机制处理。
-----> JUC:ConcurrentHashMap

HashMap集合源码相关

1. 说说HahMap底层实现(新*股份、顺*、猫*娱乐)

1
2
3
4
5
6
7
8
9
10
11
12
类似问题:
> HashMap的实现讲一下?(腾*,上海**网络)
> 说说HashMap的底层执行原理?(滴*,纬*软件,上海*想,*昂,*蝶**云,宇*科技,*东数科,猎*网)
> 详细说一下 HashMap 的 put 过程(*度)
> Java中的HashMap的工作原理是什么?(北京中**译咨询)
> 集合类的HashMap底层(安全不安全,扩容,初始大小,添加删除查询是怎么操作的,底层是什么组成的)(湖**利软件)
> HashMap 的存储过程(爱*信、杭州*智)
> Hashmap底层实现及构造(汇**通、猎*、苏州博*讯动、上海*进天下、北京博*软件、*科软、大连*点科技、中*亿达、德*物流、天*伟业、猫*娱乐)
> HashMap的实现原理(腾*、阿*)
> HaspMap底层讲一讲(*米)
> 说一下HashMap的实现,扩容机制?(*节)
> 讲一下 HashMap 中 put 方法过程?(阿*)

略。建议以JDK8为主说明。

2. HashMap初始值16,临界值12是怎么算的(软**力)

16从底层源码的构造器中看到的。

12:threshold,使用数组的长度*加载因子(loadFactor)

3. HashMap长度为什么是2的幂次方(国*时代)

为了方便计算要添加的元素的底层的索引i。

4. HashMap怎么计算哈希值和索引?扩容机制?怎么解决hash冲突?(*软国际、中软*腾)

1
2
3
4
类似问题:
> HashMap key的哈希冲突了怎么做(新*股份)
> HashMap的默认大小是多少,以及扩容机制(顺*、凡*科技)
> 讲一下HashMap的扩容机制?(好实*)

5. HashMap底层是数组+链表,有数组很快了,为什么加链表?(润*软件)

因为产生了哈希冲突。解决方案,使用链表的方式。保证要添加的元素仍然在索引i的位置上。

6. HashMap为什么长度达到一定的长度要转化为红黑树(*度)

1
2
类似问题:
> HashMap为什么用红黑树(*软国际)

红黑树的常用操作的时间复杂度O(logn),比单向链表的O(n)效率高。

7. HashMap什么时候扩充为红黑树,什么时候又返回到链表?(汉*)

1
2
3
4
类似问题:
> HashMap什么时候转换为红黑树(杭州*智公司)
> 当HashMap中相同hashcode值的数据超过多少时会转变成红黑树?(百*云创)
> 什么时候是数据+链表,什么时候是红黑树(*软国际)

索引i的位置的链表长度超过8且数组长度达到64,需要索引i位置要变成红黑树。

当索引i的位置元素的个数低于6时,要红黑树结构转为单向链表。为什么?节省空间。

8. 在 JDK1.8中,HashMap的数据结构与1.7相比有什么变化,这些变化的好处在哪里?(海*科)

1
2
3
4
5
6
7
8
9
10
11
12
① 在jdk8中,当我们创建了HashMap实例以后,底层并没有初始化table数组。当首次添加(key,value)时,进行判断,
如果发现table尚未初始化,则对数组进行初始化。
② 在jdk8中,HashMap底层定义了Node内部类,替换jdk7中的Entry内部类。意味着,我们创建的数组是Node[]
③ 在jdk8中,如果当前的(key,value)经过一系列判断之后,可以添加到当前的数组角标i中。如果此时角标i位置上有
元素。在jdk7中是将新的(key,value)指向已有的旧的元素(头插法),而在jdk8中是旧的元素指向新的
(key,value)元素(尾插法)。 "七上八下"
④ jdk7:数组+单向链表
jk8:数组+单向链表 + 红黑树
什么时候会使用单向链表变为红黑树:如果数组索引i位置上的元素的个数达到8,并且数组的长度达到64时,我们就将此索引i位置上
的多个元素改为使用红黑树的结构进行存储。(为什么修改呢?红黑树进行put()/get()/remove()
操作的时间复杂度为O(logn),比单向链表的时间复杂度O(n)的好。性能更高。
什么时候会使用红黑树变为单向链表:当使用红黑树的索引i位置上的元素的个数低于6的时候,就会将红黑树结构退化为单向链表。

9. HashMap的get()方法的原理?(顺*)

参考put()

hashCode和equals

1. hashcode和equals区别?(海*供应链管理)

2. hashCode() 与 equals() 生成算法、方法怎么重写?(阿*校招)

进行equals()判断使用的属性,通常也都会参与到hashCode()的计算中。

尽量保证hashCode()的一致性。(使用IDEA自动生成,hashCode()自动使用相关的算法。

3. 说一下equals和==的区别,然后问equals相等hash值一定相等吗?hash值相等equals一定相等吗?(南*电网、上海*智网络)

equals相等hash值一定相等吗? 是

hash值相等equals一定相等吗?不一定

Set集合源码相关

1. HashSet存放数据的方式?(拓*软件)

底层使用HashMap。说一下HashMap

2. Set是如何实现元素的唯一性?(湖**利软件)

3. 用哪两种方式来实现集合的排序(凡*科技)

1
2
类似问题:
> 集合怎么排序?(北京中**信科技)

自然排序、定制排序。

File类与IO流

File类的使用

  • 位于java.io包下
  • File类的一个实例对应着磁盘上的一个文件或文件目录。 —-> “万事万物皆对象”
  • (熟悉)File的实例化、常用的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
构造器
* public File(String pathname) :以 pathname 为路径创建 File 对象,可以是绝对路径或者相对路径,如果 pathname 是相对路径,则默认的当前路径在系统属性user.dir 中存储。
* public File(String parent, String child) :以 parent 为父路径,child 为子路径创建 File 对象。
* public File(File parent, String child) :根据一个父 File 对象和子文件路径创建 File 对象。
获取文件和目录基本信息
* public String getName() :获取名称
* public String getPath() :获取路径
* public String getAbsolutePath():获取绝对路径
* public File getAbsoluteFile():获取绝对路径表示的文件
* public String getParent():获取上层文件目录路径。若无,返回null
* public long length() :获取文件长度(即:字节数)。不能获取目录的长度。
* public long lastModified() :获取最后一次的修改时间,毫秒值
列出目录的下一级
* public String[] list() :返回一个String数组,表示该File目录中的所有子文件或目录。
* public File[] listFiles() :返回一个File数组,表示该File目录中的所有的子文件或目录。
File类的重命名功能
* public boolean renameTo(File dest):把文件重命名为指定的文件路径。
判断功能的方法
* public boolean exists() :此File表示的文件或目录是否实际存在。
* public boolean isDirectory() :此File表示的是否为目录。
* public boolean isFile() :此File表示的是否为文件。
* public boolean canRead() :判断是否可读
* public boolean canWrite() :判断是否可写
* public boolean isHidden() :判断是否隐藏
创建、删除功能
* public boolean createNewFile() :创建文件。若文件存在,则不创建,返回false。
* public boolean mkdir() :创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建。
* public boolean mkdirs() :创建文件目录。如果上层文件目录不存在,一并创建。
* public boolean delete() :删除文件或者文件夹
删除注意事项:① Java中的删除不走回收站。② 要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录。
  • File类中声明了新建、删除、获取名称、重命名等方法,并没有涉及到文件内容的读写操作。要想实现文件内容的读写,我们就需要使用io流。
  • File类的对象,通常是作为io流操作的文件的端点出现的。代码层面,将File类的对象作为参数传递到IO流相关类的构造器中。

IO流的概述

  • IO流的分类
    • 流向:输入流、输出流
    • 处理数据单位:字节流、字符流
    • 流的角色:节点流、处理流
  • IO的4个抽象基类:InputStream \ OutputStream \ Reader \ Writer

节点流之一:文件流

  • FileInputStream \ FileOutputStream \ FileReader \ FileWriter
  • 执行步骤:
    • 第1步:创建读取或写出的File类的对象
    • 第2步:创建输入流或输出流
    • 第3步:具体的读入或写出的过程。
      读入:read(char[] cbuffer)
      写出:write(String str) / write(char[] cbuffer,0,len)
    • 第4步:关闭流资源,避免内存泄漏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
    /*
* 需求:复制一份hello.txt文件,命名为hello_copy.txt
* */
@Test
public void test5() {
FileReader fr = null;
FileWriter fw = null;

try {
//1. 创建File类的对象
File srcFile = new File("hello.txt");
File destFile = new File("hello_copy.txt");

//2. 创建输入流、输出流
fr = new FileReader(srcFile);
fw = new FileWriter(destFile);

//3. 数据的读入和写出的过程
char[] cbuffer = new char[5];
int len;//记录每次读入到cbuffer中的字符的个数
while ((len = fr.read(cbuffer)) != -1) {
//write(char[] cbuffer,int fromIndex,int len)
fw.write(cbuffer, 0, len); //正确的
// fw.write(cbuffer); //错误的
}

System.out.println("复制成功");
} catch (IOException e) {
e.printStackTrace();
} finally {
//4. 关闭流资源
//方式1:
// try {
//if (fw != null)
// fw.close();
// } catch (IOException e) {
// e.printStackTrace();
// }finally {
//
// try {
//if (fr != null)
// fr.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
//方式2:
try {
if (fw != null)
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fr != null)
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

  • 注意点:
    ① 因为涉及到流资源的关闭操作,所以出现异常的话,需要使用try-catch-finally的方式来处理异常
    ② 对于输入流来讲,要求File类的对象对应的物理磁盘上的文件必须存在。否则,会报FileNotFoundException
    对于输出流来讲,File类的对象对应的物理磁盘上的文件可以不存在。
    > 如果此文件不存在,则在输出的过程中,会自动创建此文件,并写出数据到此文件中。
    > 如果此文件存在,使用 FileWriter(File file) 或 FileWriter(File file,false):
    输出数据过程中,会新建同名的文件对现有的文件进行覆盖。
    FileWriter(File file,true) : 输出数据过程中,会在现有的文件的末尾追加写出内容。

处理流之一:缓冲流

  • BufferedInputStream \ BufferedOutputStream \ BufferedReader \ BufferedWriter
  • 作用:实现更高效的读写数据的操作

处理流之二:转换流

  • 层次1:熟悉转换流的使用
    • InputStreamReader、OutputStreamWriter:
      • InputStreamReader:将一个输入型的字节流转换为输入型的字符流。
      • OutputStreamWriter:将一个输出型的字符流转换为输出型的字节流。
  • 层次2:(掌握)字符的编码和解码的过程、常用的字符集
    • 解决相关的问题:读写字符出现乱码!本质问题:使用的解码集与编码集不一致。

处理流之三:对象流

  • 层次1:熟悉对象流的使用
    • ObjectInputStream:反序列化时需要使用的api,将文件中的数据或网络传输过来的数据还原为内存中的Java对象
    • ObjectOutputStream:序列化时需要使用的api,将内存中的Java对象保存在文件中或通过网络传输出去
  • 层次2:对象的序列化机制
    • 对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。//当其它程序获取了这种二进制流,就可以恢复成原来的Java对象。
    • 自定义类要想实现序列化机制,需要满足:
      ① 自定义类需要实现接口:Serializable
      ② 要求自定义类声明一个全局常量: static final long serialVersionUID = 42234234L;用来唯一的标识当前的类。
      ③ 要求自定义类的各个属性也必须是可序列化的。

      对于基本数据类型的属性:默认就是可以序列化的
      对于引用数据类型的属性:要求实现Serializable接口

其它流的使用

  • 了解:数据流:DataInputStream 、DataOutputStream
  • 了解:标准的输入流、标准的输出流:System.in 、System.out
  • 了解:打印流:PrintStream、PrintWriter

企业真题(十五)

IO流概述

1. 谈谈Java IO里面的常用类,字节流,字符流(银*数据)

2. Java 中有几种类型的流?JDK为每种类型的流提供一些抽象类以供继承,请说出他们分别是哪些类?(上海*厦*联网、极*科技)

InputStream \ OutputStream \ Reader \ Writer

3. 流一般需不需要关闭?如果关闭的话用什么方法?处理流是怎么关闭的?(银*数据)

需要。close()

处理流在关闭过程中,也会关闭内部的流。

4. OutputStream里面的write()是什么意思?(君*科技)

数据写出的意思。

缓冲流

1. BufferedReader属于哪种流?他主要是用来做什么的?(国*电网)

2. 什么是缓冲区?有什么作用?(北京中油**)

内部提供了一个数组,将读取或要写出的数据,现在此数组中缓存。达到一定程度时,集中性的写出。

作用:减少与磁盘的交互,进而提升读写效率。

转换流

1. 字节流和字符流是什么?怎么转换?(北京蓝*、*海*供应链管理)

序列化

1. 什么是Java序列化,如何实现(君*科技、上海*厦物联网)

1
2
对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,
或通过网络将这种二进制流传输到另一个网络节点。//当其它程序获取了这种二进制流,就可以恢复成原来的Java对象。

2. Java有些类中为什么需要实现Serializable接口?(阿*校招)

便于此类的对象实现序列化操作。

网络编程

网络编程概述

  • 计算机网络:把分布在不同地理区域的计算机与专门的外部设备用通信线路互连成一个规模大、功能强的网络系统,从而使众多的计算机可以方便地互相传递信息、共享硬件、软件、数据信息等资源。
  • 网络编程的目的:直接或间接地通过网络协议与其它计算机实现数据交换,进行通讯。
  • 需要解决的三个问题:
    • 问题1:如何准确地定位网络上一台或多台主机
    • 问题2:如何定位主机上的特定的应用
    • 问题3:找到主机后,如何可靠、高效地进行数据传输

要素1:IP地址

  • 使用具体的一个ip地址对应具体的一个互联网上的主机
  • IP分类:
    • 角度一:IPv4(占用4个字节)、IPv6(占用16个字节)
    • 角度二:公网地址、私网地址(或局域网,以192.168开头)
  • 域名:便捷的记录ip地址
  • 使用InetAddress类表示IP地址
    • 实例化:getByName(String host) 、getLocalHost()
    • 常用方法:getHostName() 、getHostAddress()

要素2:端口号

  • 用于区分同一台主机上的不同的进程,可以唯一标识主机中的进程(应用程序)
  • 不同的进程分配不同的端口号
  • 范围:0-65535

要素3:网络通信协议

为了实现可靠而高效的数据传输。

  • 这里有两套参考模型
    • OSI参考模型:将网络分为7层,模型过于理想化,未能在因特网上进行广泛推广
    • TCP/IP参考模型(或TCP/IP协议):将网络分为4层:应用层、传输层、网络层、物理+数据链路层,事实上的国际标准。
  • 在传输层中涉及到两个协议:TCP、UDP。二者的对比
    • TCP:可靠的连接(发送数据前,需要三次握手、四次挥手),进行大数据量的传输,效率低。
    • UDP:不可靠的连接(发送前,不需要确认对方是否在)、使用数据报传输(限制在64kb以内)、效率高。
  • TCP的三次握手、四次挥手
    • TCP 协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。第一次握手,客户端向服务器端发起 TCP 连接的请求;第二次握手,服务器端发送针对客户端 TCP 连接请求的确认;第三次握手,客户端发送确认的确认。
    • TCP 协议中,在发送数据结束后,释放连接时需要经过四次挥手。第一次挥手:客户端向服务器端提出结束连接,让服务器做最后的准备工作。此时,客户端处于半关闭状态,即表示不再向服务器发送数据了,但是还可以接受数据;第二次挥手:服务器接收到客户端释放连接的请求后,会将最后的数据发给客户端。并告知上层的应用进程不再接收数据;第三次挥手:服务器发送完数据后,会给客户端发送一个释放连接的报文。那么客户端接收后就知道可以正式释放连接了;第四次挥手:客户端接收到服务器最后的释放连接报文后,要回复一个彻底断开的报文。这样服务器收到后才会彻底释放连接。这里客户端,发送完最后的报文后,会等待 2MSL(Maximum Segment Lifetime),因为有可能服务器没有收到最后的报文,那么服务器迟迟没收到,就会再次给客户端发送释放连接的报文,此时客户端在等待时间范围内接收到,会重新发送最后的报文,并重新计时。如果等待 2MSL 后,没有收到,那么彻底断开。

TCP网络编程

开发步骤:

  • 客户端程序:
    1. 创建 Socket :根据指定服务端的 IP 地址或端口号构造 Socket 类对象。若服务器端响应,则建立客户端到服务器的通信线路。若连接失败,会出现异常。
    2. 打开连接到 Socket 的输入/ 出流: 使用 getInputStream()方法获得输入流,使用getOutputStream()方法获得输出流,进行数据传输
    3. 按照一定的协议对 Socket 进行读/ 写操作:通过输入流读取服务器放入线路的信息(但不能读取自己放入线路的信息),通过输出流将信息写入线路。
    4. 关闭 Socket :断开客户端到服务器的连接,释放线路
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//客户端
@Test
public void client() {
Socket socket = null;
OutputStream os = null;
try {
//1. 创建一个Socket
InetAddress inetAddress = InetAddress.getByName("192.168.21.107"); //声明对方的ip地址
int port = 8989;//声明对方的端口号
socket = new Socket(inetAddress, port);

//2. 发送数据
os = socket.getOutputStream();
os.write("你好,我是客户端,请多多关照".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
//3. 关闭socket、关闭流
try {
if (socket != null)
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (os != null) {
os.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}

}

  • 服务器端程序:
    1. 调用 ServerSocket(int port) :创建一个服务器端套接字,并绑定到指定端口上。用
      于监听客户端的请求。
    2. 调用 accept() :监听连接请求,如果客户端请求连接,则接受连接,返回通信套接
      字对象。
    3. 调用 该 Socket 类对象的 getOutputStream() 和 getInputStream () :获取输出流和
      输入流,开始网络数据的发送和接收。
    4. 关闭 Socket 对象:客户端访问结束,关闭通信套接字。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//服务端
@Test
public void server() {
ServerSocket serverSocket = null;
Socket socket = null; //阻塞式的方法
InputStream is = null;
try {
//1. 创建一个ServerSocket
int port = 8989;
serverSocket = new ServerSocket(port);

//2. 调用accept(),接收客户端的Socket
socket = serverSocket.accept();
System.out.println("服务器端已开启");

System.out.println("收到了来自于" + socket.getInetAddress().getHostAddress() + "的连接");

//3. 接收数据
is = socket.getInputStream();
byte[] buffer = new byte[5];
int len;
ByteArrayOutputStream baos = new ByteArrayOutputStream(); //内部维护了一个byte[]
while ((len = is.read(buffer)) != -1) {
//错误的,可能会出现乱码。
// String str = new String(buffer, 0, len);
// System.out.print(str);

//正确的
baos.write(buffer,0,len);
}

System.out.println(baos.toString());

System.out.println("\n数据接收完毕");
} catch (IOException e) {
e.printStackTrace();
} finally {
//4. 关闭Socket、ServerSocket、流
try {
if (socket != null) {
socket.close();

}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (serverSocket != null) {
serverSocket.close();

}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (is != null) {
is.close();

}
} catch (IOException e) {
e.printStackTrace();
}
}
}

  • 聊天室案例(与多线程结合)

UDP网络编程

开发步骤:

  • 发送端程序:
    1. 创建 DatagramSocket :默认使用系统随机分配端口号。
    2. 创建 DatagramPacket:将要发送的数据用字节数组表示,并指定要发送的数据长度,接收方的 IP 地址和端口号。
    3. 调用 该 DatagramSocket 类对象的 send 方法 :发送数据报 DatagramPacket 对象。
    4. 关闭 DatagramSocket 对象:发送端程序结束,关闭通信套接字。
  • 接收端程序:
    1. 创建 DatagramSocket :指定监听的端口号。
    2. 创建 DatagramPacket:指定接收数据用的字节数组,起到临时数据缓冲区的效果,并指定最大可以接收的数据长度。
    3. 调用 该 DatagramSocket 类对象的 receive 方法 :接收数据报 DatagramPacket 对象。
    4. 关闭 DatagramSocket :接收端程序结束,关闭通信套接字。

URL编程

  • Java后台:将写好的Java程序部署在Tomcat服务器。启动Tomcat服务器。

  • 前台:使用浏览器进行访问。需要使用url。(HTML+CSS+JavaScript)

  • URL的作用:定位互联网上某一资源的地址。

  • URL的格式:

1
2
http://192.168.21.107:8080/examples/abcd.jpg?name=Tom   ---> "万事万物皆对象"
应用层协议 ip地址 端口号 资源地址 参数列表
  • 使用URL实现数据的下载(了解)

企业真题(十六)

1. TCP协议和UDP协议的区别(华**为)

2. 简单说说TCP协议的三次握手与四次挥手机制 (*科软)

反射机制

反射的概述

通过使用反射前后的例子的对比,回答:

  1. 面向对象中创建对象,调用指定结构(属性、方法)等功能,可以不使用反射,也可以使用反射。请问有什么区别?

不使用反射,我们需要考虑封装性。比如:出了Person类之后,就不能调用Person类中私有的结构
使用反射,我们可以调用运行时类中任意的构造器、属性、方法。包括了私有的属性、方法、构造器。

  1. 以前创建对象并调用方法的方式,与现在通过反射创建对象并调用方法的方式对比的话,哪种用的多?
    场景是什么?

从我们作为程序员开发者的角度来讲,我们开发中主要是完成业务代码,对于相关的对象、方法的调用都是确定的。
所以,我们使用非反射的方式多一些。

因为反射体现了动态性(可以在运行时动态的获取对象所属的类,动态的调用相关的方法),所以我们在设计框架的时候,
会大量的使用反射。意味着,如果大家需要学习框架源码,那么就需要学习反射。

框架 = 注解 + 反射 + 设计模式

  1. 单例模式的饿汉式和懒汉式中,私有化类的构造器了! 此时通过反射,可以创建单例模式中类的多个对象吗?

是的!

  1. 通过反射,可以调用类中私有的结构,是否与面向对象的封装性有冲突?是不是Java语言设计存在Bug?

不存在bug!

封装性:体现的是是否建议我们调用内部api的问题。比如,private声明的结构,意味着不建议调用。
反射:体现的是我们能否调用的问题。因为类的完整结构都加载到了内存中,所有我们就有能力进行调用。

  • Java给我们提供了一套API,使用这套API我们可以在运行时动态的获取指定对象所属的类,创建运行时类的对象,调用指定的结构(属性、方法)等。
  • API:
    • java.lang.Class:代表一个类
    • java.lang.reflect.Method:代表类的方法
    • java.lang.reflect.Field:代表类的成员变量
    • java.lang.reflect.Constructor:代表类的构造器
    • … …
  • 反射的优点和缺点
    • 优点:
      • 提高了Java程序的灵活性和扩展性,降低了耦合性,提高自适应能力

      • 允许程序创建和控制任何类的对象,无需提前硬编码目标类

    • 缺点:
      • 反射的性能较低
        • 反射机制主要应用在对灵活性和扩展性要求很高的系统框架上
      • 反射会模糊程序内部逻辑,可读性较差
  • 反射,平时开发中,我们使用并不多。主要是在框架的底层使用。

Class:反射的源头

  • Class的理解 (掌握)

    1
    2
    针对于编写好的.java源文件进行编译(使用javac.exe),会生成一个或多个.class字节码文件。接着,我们使用
    java.exe命令对指定的.class文件进行解释运行。这个解释运行的过程中,我们需要将.class字节码文件加载(使用类的加载器)到内存中(存放在方法区)。加载到内存中的.class文件对应的结构即为Class的一个实例。
  • 获取Class的实例的几种方式(前三种)

    • 类.class
    • 对象.getClass()
    • (使用较多)Class调用静态方法forName(String className)
    • (了解)使用ClassLoader的方法loadClass(String className)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
      
    //1.调用运行时类的静态属性:class
    Class clazz1 = User.class;

    //2. 调用运行时类的对象的getClass()
    User u1 = new User();
    Class clazz2 = u1.getClass();

    //3. 调用Class的静态方法forName(String className)
    String className = "com.atguigu02._class.User"; //全类名
    Class clazz3 = Class.forName(className);

    System.out.println(clazz1 == clazz2);//true
    System.out.println(clazz1 == clazz3);//true

    //4. 使用类的加载器的方式 (了解)
    Class clazz4 = ClassLoader.getSystemClassLoader().loadClass("com.atguigu02._class.User");
    System.out.println(clazz1 == clazz4);//true

  • Class 可以指向哪些结构。

    1
    2
    3
    4
    5
    6
    7
    8
    简言之,所有Java类型!
    (1)class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
    (2)interface:接口
    (3)[]:数组
    (4)enum:枚举
    (5)annotation:注解@interface
    (6)primitive type:基本数据类型
    (7)void

类的加载过程、类的加载器(理解)

  • 类的加载过程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    过程1:类的装载(loading)
    将类的class文件读入内存,并为之创建一个java.lang.Class对象。此过程由类加载器完成

    过程2:链接(linking)
    > 验证(Verify):确保加载的类信息符合JVM规范,例如:以cafebabe开头,没有安全方面的问题。
    > 准备(Prepare):正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
    > 解析(Resolve):虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。

    过程3:初始化(initialization)
    执行类构造器<clinit>()方法的过程。
    类构造器<clinit>()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。
  • 类的加载器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    作用:负责类的加载,并对应于一个Class的实例。

    分类(分为两种):
    > BootstrapClassLoader:引导类加载器、启动类加载器
    > 使用C/C++语言编写的,不能通过Java代码获取其实例
    > 负责加载Java的核心库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容)

    > 继承于ClassLoader的类加载器
    > ExtensionClassLoader:扩展类加载器
    > 负责加载从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库
    > SystemClassLoader/ApplicationClassLoader:系统类加载器、应用程序类加载器
    > 我们自定义的类,默认使用的类的加载器。
    > 用户自定义类的加载器
    > 实现应用的隔离(同一个类在一个应用程序中可以加载多份);数据的加密。
  • 使用类的加载器获取流,并读取配置文件信息:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    /*
    * 需求:通过ClassLoader加载指定的配置文件
    * */
    @Test
    public void test3() throws IOException {
    Properties pros = new Properties();
    //其他方式读取的文件的默认路径为:当前的module
    //通过类的加载器读取的文件的默认的路径为:当前module下的src下
    InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("info1.properties");

    pros.load(is);

    String name = pros.getProperty("name");
    String pwd = pros.getProperty("password");
    System.out.println(name + ":" +pwd);
    }

反射的应用1:创建运行时类的对象(重点)

1
2
3
4
5
6
Class clazz = Person.class;

//创建Person类的实例
Person per = (Person) clazz.newInstance();

System.out.println(per);
1
2
3
要想创建对象成功,需要满足:
条件1:要求运行时类中必须提供一个空参的构造器
条件2:要求提供的空参的构造器的权限要足够。

反射的应用2:获取运行时类所有的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(了解)获取运行时类的内部结构1:所有属性、所有方法、所有构造器
· getFields():获取到运行时类本身及其所有的父类中声明为public权限的属性
· getDeclaredFields():获取当前运行时类中声明的所有属性
· getMethods():获取到运行时类本身及其所有的父类中声明为public权限的方法
· getDeclaredMethods():获取当前运行时类中声明的所有方法
(熟悉)获取运行时类的内部结构2:父类、接口们、包、带泛型的父类、父类的泛型等
1. 获取运行时类的父类 getSuperclass()
2. 获取运行时类实现的接口 getInterfaces()
3. 获取运行时类所在的包 getPackage()
4. 获取运行时类的带泛型的父类 getGenericSuperclass()
5. 获取运行时类的父类的泛型
@Test
public void test5() throws ClassNotFoundException {
Class clazz = Class.forName("com.atguigu03.reflectapply.data.Person");
//获取带泛型的父类(Type是一个接口,Class实现了此接口
Type superclass = clazz.getGenericSuperclass();
//如果父类是带泛型的,则可以强转为ParameterizedType
ParameterizedType paramType = (ParameterizedType) superclass;
//调用getActualTypeArguments()获取泛型的参数,结果是一个数组,因为可能有多个泛型参数。
Type[] arguments = paramType.getActualTypeArguments();
//获取泛型参数的名称
System.out.println(((Class)arguments[0]).getName());
}

反射的应用3:调用指定的结构(重点)

1
2
3
4
5
1 调用指定的属性(步骤)
步骤1.通过Class实例调用getDeclaredField(String fieldName),获取运行时类指定名的属性
步骤2. setAccessible(true):确保此属性是可以访问的
步骤3. 通过Filed类的实例调用get(Object obj) (获取的操作)
或 set(Object obj,Object value) (设置的操作)进行操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void test2() throws Exception {
Class clazz = Person.class;

//
Person per = (Person) clazz.newInstance();

//1.通过Class实例调用getDeclaredField(String fieldName),获取运行时类指定名的属性
Field nameField = clazz.getDeclaredField("name");

//2. setAccessible(true):确保此属性是可以访问的
nameField.setAccessible(true);

//3. 通过Filed类的实例调用get(Object obj) (获取的操作)
// 或 set(Object obj,Object value) (设置的操作)进行操作。
nameField.set(per,"Tom");
System.out.println(nameField.get(per));
}
1
2
3
4
5
6
2 调用指定的方法(步骤)
步骤1.通过Class的实例调用getDeclaredMethod(String methodName,Class ... args),获取指定的方法
步骤2. setAccessible(true):确保此方法是可访问的
步骤3.通过Method实例调用invoke(Object obj,Object ... objs),即为对Method对应的方法的调用。
invoke()的返回值即为Method对应的方法的返回值
特别的:如果Method对应的方法的返回值类型为void,则invoke()返回值为null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

@Test
public void test4() throws Exception {
Class clazz = Person.class;

Person per = (Person) clazz.newInstance();

//1.通过Class的实例调用getDeclaredMethod(String methodName,Class ... args),获取指定的方法
Method showNationMethod = clazz.getDeclaredMethod("showNation",String.class,int.class);

//2. setAccessible(true):确保此方法是可访问的
showNationMethod.setAccessible(true);

//3.通过Method实例调用invoke(Object obj,Object ... objs),即为对Method对应的方法的调用。
//invoke()的返回值即为Method对应的方法的返回值
//特别的:如果Method对应的方法的返回值类型为void,则invoke()返回值为null
Object returnValue = showNationMethod.invoke(per,"CHN",10);
System.out.println(returnValue);
}

1
2
3
4
3 调用指定的构造器(步骤)
步骤1.通过Class的实例调用getDeclaredConstructor(Class ... args),获取指定参数类型的构造器
步骤2.setAccessible(true):确保此构造器是可以访问的
步骤3.通过Constructor实例调用newInstance(Object ... objs),返回一个运行时类的实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void test6() throws Exception {

Class clazz = Person.class;

//1.通过Class的实例调用getDeclaredConstructor(Class ... args),获取指定参数类型的构造器
Constructor constructor = clazz.getDeclaredConstructor(String.class, int.class);

//2.setAccessible(true):确保此构造器是可以访问的
constructor.setAccessible(true);

//3.通过Constructor实例调用newInstance(Object ... objs),返回一个运行时类的实例。
Person per = (Person) constructor.newInstance("Tom", 12);

System.out.println(per);

}

反射的应用4:注解的使用(了解)

在框架中用的比较多。

体会:反射的动态性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class ReflectTest {

//体会:静态性
public Person getInstance(){
return new Person();
}

//体会:反射的动态性
//举例1:
public <T> T getInstance(String className) throws Exception {

Class clazz = Class.forName(className);

Constructor con = clazz.getDeclaredConstructor();
con.setAccessible(true);

return (T) con.newInstance();

}

@Test
public void test1() throws Exception {
Person p1 = getInstance();
System.out.println(p1);

String className = "com.atguigu04.other.dynamic.Person";
Person per1 = getInstance(className);
System.out.println(per1);

String className1 = "java.util.Date";
Date date1 = getInstance(className1);
System.out.println(date1);
}

//体会:反射的动态性
//举例2:
public Object invoke(String className,String methodName) throws Exception {
//1. 创建全类名对应的运行时类的对象
Class clazz = Class.forName(className);

Constructor con = clazz.getDeclaredConstructor();
con.setAccessible(true);

Object obj = con.newInstance();

//2. 获取运行时类中指定的方法,并调用
Method method = clazz.getDeclaredMethod(methodName);
method.setAccessible(true);
return method.invoke(obj);
}

@Test
public void test2() throws Exception {
String className = "com.atguigu04.other.dynamic.Person";
String methodName = "show";

Object returnValue = invoke(className,methodName);
System.out.println(returnValue);
}

}

企业真题(十七)

反射概述

1. 对反射了解吗?反射有什么好处?为什么需要反射?(微*银行)

1
2
3
4
类似问题:
> Java反射的作用是什么?(三*重工、上海*和网络)
> Java反射机制的作用有什么?(上海明*物联网)
> 反射的具体用途?(阿***芝*信用项目组)

2. 反射的使用场合和作用、及其优缺点(*软国际)

1
2
3
类似问题:
> 反射机制的优缺点(君*科技)
> Java反射你怎么用的?(吉*航空)

3. 实现Java反射的类有什么?(君*科技)

1
2
类似问题:
> Java反射 API 有几类?(北京*蓝)

问API。

4. 反射是怎么实现的?(上海立*网络)

从Class说起。

Class的理解

1. Class类的作用?生成Class对象的方法有哪些?(顺*)

反射的源头。 主要有三种。

2. Class.forName(“全路径”) 会调用哪些方法 ? 会调用构造方法吗?加载的类会放在哪?(上*银行外包)

Class.forName() 会执行执行类构造器()方法。

不会调用构造方法

加载的类放在方法区。

类的加载

1. 类加载流程(汇**通、同*顺、凡*科技)

创建对象

1. 说一下创建对象的几种方法?(华油***集团、*科软、凡*科技)

1
2
类似问题:
> 除了使用new创建对象之外,还可以用什么方法创建对象?(*云网络)

image-20221214145240412

2. 如何找到对象实际类的?(*度)

对象.getClass();

1
2
3
Object obj = new Date();

obj.getClass();// 获取到的是Date。

3. Java反射创建对象效率高还是通过new创建对象的效率高?(三*重工)

new 的方式。

调用属性、方法

1. 如何利用反射机制来访问一个类的方法?(神州**软件)

1
2
3
4
5
6
调用指定的方法(步骤)
步骤1.通过Class的实例调用getDeclaredMethod(String methodName,Class ... args),获取指定的方法
步骤2. setAccessible(true):确保此方法是可访问的
步骤3.通过Method实例调用invoke(Object obj,Object ... objs),即为对Method对应的方法的调用。
invoke()的返回值即为Method对应的方法的返回值
特别的:如果Method对应的方法的返回值类型为void,则invoke()返回值为null

2. 说一下Java反射获取私有属性,如何改变值?(阿****麻信用项目组)

1
2
3
4
5
调用指定的属性(步骤)
步骤1.通过Class实例调用getDeclaredField(String fieldName),获取运行时类指定名的属性
步骤2. setAccessible(true):确保此属性是可以访问的
步骤3. 通过Filed类的实例调用get(Object obj) (获取的操作)
或 set(Object obj,Object value) (设置的操作)进行操作。
1
针对于核心源码的api,内部的私有的结构在jdk17中就不可以通过反射调用了。

JDK8-17新特性

Java版本迭代概述

发行版本 发行时间 备注
Java 1.0 1996.01.23 Sun公司发布了Java的第一个开发工具包
Java 5.0 2004.09.30 ①版本号从1.4直接更新至5.0;②平台更名为JavaSE、JavaEE、JavaME
Java 8.0 2014.03.18 此版本是继Java 5.0以来变化最大的版本。是长期支持版本(LTS
Java 9.0 2017.09.22 此版本开始,每半年更新一次
Java 10.0 2018.03.21
Java 11.0 2018.09.25 JDK安装包取消独立JRE安装包,是长期支持版本(LTS
Java 12.0 2019.03.19
Java17.0 2021.09 发布Java 17.0,版本号也称为21.9,是长期支持版本(LTS
Java19.0 2022.09 发布Java19.0,版本号也称为22.9。

从Java 9 这个版本开始,Java 的计划发布周期是 6个月

这意味着Java的更新从传统的以特性驱动的发布周期,转变为以时间驱动的发布模式,并且承诺不会跳票。通过这样的方式,开发团队可以把一些关键特性尽早合并到 JDK 之中,以快速得到开发者反馈,在一定程度上避免出现像 Java 9 两次被迫延迟发布的窘况。

针对企业客户的需求,Oracle 将以三年为周期发布长期支持版本(long term support)。

Oracle 的官方观点认为:与 Java 7->8->9 相比,Java 9->10->11的升级和 8->8u20->8u40 更相似。

新模式下的 Java 版本发布都会包含许多变更,包括语言变更JVM 变更,这两者都会对 IDE、字节码库和框架产生重大影响。此外,不仅会新增其他 API,还会有 API被删除(这在 Java 8 之前没有发生过)。

各版本介绍

jdk 9

Java 9 提供了超过150项新功能特性,包括备受期待的模块化系统、可交互的 REPL 工具:jshell,JDK 编译工具,Java 公共 API 和私有代码,以及安全增强、扩展提升、性能管理改善等。

特性太多,查看链接:

https://openjdk.java.net/projects/jdk9/

jdk 10

https://openjdk.java.net/projects/jdk/10/

286: Local-Variable Type Inference 局部变量类型推断
296: Consolidate the JDK Forest into a Single Repository JDK库的合并
304: Garbage-Collector Interface 统一的垃圾回收接口
307: Parallel Full GC for G1 为G1提供并行的Full GC
310: Application Class-Data Sharing 应用程序类数据(AppCDS)共享
312: Thread-Local Handshakes ThreadLocal握手交互
313: Remove the Native-Header Generation Tool (javah) 移除JDK中附带的javah工具
314: Additional Unicode Language-Tag Extensions 使用附加的Unicode语言标记扩展
316: Heap Allocation on Alternative Memory Devices 能将堆内存占用分配给用户指定的备用内存设备
317: Experimental Java-Based JIT Compiler 使用Graal基于Java的编译器

319: Root Certificates 根证书
322: Time-Based Release Versioning 基于时间定于的发布版本

jdk 11

https://openjdk.java.net/projects/jdk/11/

181: Nest-Based Access Control 基于嵌套的访问控制
309: Dynamic Class-File Constants 动态类文件常量
315: Improve Aarch64 Intrinsics 改进 Aarch64 Intrinsics
318: Epsilon: A No-Op Garbage Collector Epsilon — 一个No-Op(无操作)的垃圾收集器
320: Remove the Java EE and CORBA Modules 删除 Java EE 和 CORBA 模块
321: HTTP Client (Standard) HTTPClient API
323: Local-Variable Syntax for Lambda Parameters 用于 Lambda 参数的局部变量语法
324: Key Agreement with Curve25519 and Curve448 Curve25519 和 Curve448 算法的密钥协议
327: Unicode 10
328: Flight Recorder 飞行记录仪
329: ChaCha20 and Poly1305 Cryptographic Algorithms ChaCha20 和 Poly1305 加密算法
330: Launch Single-File Source-Code Programs 启动单一文件的源代码程序
331: Low-Overhead Heap Profiling 低开销的 Heap Profiling
332: Transport Layer Security (TLS) 1.3 支持 TLS 1.3
333: ZGC: A Scalable Low-Latency Garbage Collector
   (Experimental)
可伸缩低延迟垃圾收集器
335: Deprecate the Nashorn JavaScript Engine 弃用 Nashorn JavaScript 引擎
336: Deprecate the Pack200 Tools and API 弃用 Pack200 工具和 API

jdk 12

https://openjdk.java.net/projects/jdk/12/

189:Shenandoah: A Low-Pause-Time Garbage Collector (Experimental) 低暂停时间的GC
230: Microbenchmark Suite 微基准测试套件
325: Switch Expressions (Preview) switch表达式
334: JVM Constants API JVM常量API
340: One AArch64 Port, Not Two 只保留一个AArch64实现
341: Default CDS Archives 默认类数据共享归档文件
344: Abortable Mixed Collections for G1 可中止的G1 Mixed GC
346: Promptly Return Unused Committed Memory from G1 G1及时返回未使用的已分配内存

jdk 13

https://openjdk.java.net/projects/jdk/13/

350: Dynamic CDS Archives 动态CDS档案
351: ZGC: Uncommit Unused Memory ZGC:取消使用未使用的内存
353: Reimplement the Legacy Socket API 重新实现旧版套接字API
354: Switch Expressions (Preview) switch表达式(预览)
355: Text Blocks (Preview) 文本块(预览)

jdk 14

https://openjdk.java.net/projects/jdk/14/

305: Pattern Matching for instanceof (Preview) instanceof的模式匹配
343: Packaging Tool (Incubator) 打包工具
345: NUMA-Aware Memory Allocation for G1 G1的NUMA-Aware内存分配
349: JFR Event Streaming JFR事件流
352: Non-Volatile Mapped Byte Buffers 非易失性映射字节缓冲区
358: Helpful NullPointerExceptions 实用的NullPointerExceptions
359: Records (Preview)
361: Switch Expressions (Standard) Switch表达式
362: Deprecate the Solaris and SPARC Ports 弃用Solaris和SPARC端口
363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector 删除并发标记扫描(CMS)垃圾回收器
364: ZGC on macOS
365: ZGC on Windows
366: Deprecate the ParallelScavenge + SerialOld GC Combination 弃用ParallelScavenge + SerialOld GC组合
367: Remove the Pack200 Tools and API 删除Pack200工具和API
368: Text Blocks (Second Preview) 文本块
370: Foreign-Memory Access API (Incubator) 外部存储器访问API

jdk 15

https://openjdk.java.net/projects/jdk/15/

339: Edwards-Curve Digital Signature Algorithm (EdDSA) EdDSA 数字签名算法
360: Sealed Classes (Preview) 密封类(预览)
371: Hidden Classes 隐藏类
372: Remove the Nashorn JavaScript Engine 移除 Nashorn JavaScript 引擎
373: Reimplement the Legacy DatagramSocket API 重新实现 Legacy DatagramSocket API
374: Disable and Deprecate Biased Locking 禁用偏向锁定
375: Pattern Matching for instanceof (Second Preview) instanceof 模式匹配(第二次预览)
377: ZGC: A Scalable Low-Latency Garbage Collector ZGC:一个可扩展的低延迟垃圾收集器
378: Text Blocks 文本块
379: Shenandoah: A Low-Pause-Time Garbage Collector Shenandoah:低暂停时间垃圾收集器
381: Remove the Solaris and SPARC Ports 移除 Solaris 和 SPARC 端口
383: Foreign-Memory Access API (Second Incubator) 外部存储器访问 API(第二次孵化版)
384: Records (Second Preview) Records(第二次预览)
385: Deprecate RMI Activation for Removal 废弃 RMI 激活机制

jdk 16

https://openjdk.java.net/projects/jdk/16/

338: Vector API (Incubator) Vector API(孵化器)
347: Enable C++14 Language Features JDK C++的源码中允许使用C++14的语言特性
357: Migrate from Mercurial to Git OpenJDK源码的版本控制从Mercurial (hg) 迁移到git
369: Migrate to GitHub OpenJDK源码的版本控制迁移到github上
376: ZGC: Concurrent Thread-Stack Processing ZGC:并发线程处理
380: Unix-Domain Socket Channels Unix域套接字通道
386: Alpine Linux Port 将glibc的jdk移植到使用musl的alpine linux上
387: Elastic Metaspace 弹性元空间
388: Windows/AArch64 Port 移植JDK到Windows/AArch64
389: Foreign Linker API (Incubator) 提供jdk.incubator.foreign来简化native code的调用
390: Warnings for Value-Based Classes 提供基于值的类的警告
392: Packaging Tool jpackage打包工具转正
393: Foreign-Memory Access API (Third Incubator)
394: Pattern Matching for instanceof Instanceof的模式匹配转正
395: Records Records转正
396: Strongly Encapsulate JDK Internals by Default 默认情况下,封装了JDK内部构件
397: Sealed Classes (Second Preview) 密封类

jdk 17

https://openjdk.java.net/projects/jdk/17/

306: Restore Always-Strict Floating-Point Semantics 恢复始终严格的浮点语义

356: Enhanced Pseudo-Random Number Generators 增强型伪随机数生成器

382: New macOS Rendering Pipeline 新的macOS渲染管道

391: macOS/AArch64 Port macOS/AArch64端口

398: Deprecate the Applet API for Removal 弃用Applet API后续将进行删除

403: Strongly Encapsulate JDK Internals 强封装JDK的内部API

406: Pattern Matching for switch (Preview) switch模式匹配(预览)

407: Remove RMI Activation 删除RMI激活机制

409: Sealed Classes 密封类转正

410: Remove the Experimental AOT and JIT Compiler 删除实验性的AOT和JIT编译器

411: Deprecate the Security Manager for Removal 弃用即将删除的安全管理器

412: Foreign Function & Memory API (Incubator) 外部函数和内存API(孵化特性)

414: Vector API (Second Incubator) Vector API(第二次孵化特性)

415: Context-Specific Deserialization Filters 上下文特定的反序列化过滤器

Java8新特性:Lambda表达式

场景

冗余的匿名内部类
当需要启动一个线程去完成任务时,通常会通过java.lang.Runnable接口来定义任务内容,并使用java.lang.Thread类来启动该线程。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
package com.atguigu.fp;

public class UseFunctionalProgramming {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("多线程任务执行!");
}
}).start(); // 启动线程
}
}

本着“一切皆对象”的思想,这种做法是无可厚非的:首先创建一个Runnable接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。

代码分析:

对于Runnable的匿名内部类用法,可以分析出几点内容:

  • Thread类需要Runnable接口作为参数,其中的抽象run方法是用来指定线程任务内容的核心;
  • 为了指定run的方法体,不得不需要Runnable接口的实现类;
  • 为了省去定义一个RunnableImpl实现类的麻烦,不得不使用匿名内部类;
  • 必须覆盖重写抽象run方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;
  • 而实际上,似乎只有方法体才是关键所在

Lambda 及其语法

Lambda 是一个匿名函数,我们可以把 Lambda 表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。使用它可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升

Lambda 表达式:在Java 8 语言中引入的一种新的语法元素和操作符。这个操作符为 “->” , 该操作符被称为 Lambda 操作符箭头操作符。它将 Lambda 分为两个部分:

  • 左侧:指定了 Lambda 表达式需要的参数列表
  • 右侧:指定了 Lambda 体,是抽象方法的实现逻辑,也即 Lambda 表达式要执行的功能。

语法格式一:无参,无返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void test1(){
//未使用Lambda表达式
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("我爱北京天安门");
}
};

r1.run();

System.out.println("***********************");

//使用Lambda表达式
Runnable r2 = () -> {
System.out.println("我爱北京故宫");
};

r2.run();
}

语法格式二:Lambda 需要一个参数,但是没有返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void test2(){
//未使用Lambda表达式
Consumer<String> con = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
con.accept("谎言和誓言的区别是什么?");

System.out.println("*******************");

//使用Lambda表达式
Consumer<String> con1 = (String s) -> {
System.out.println(s);
};
con1.accept("一个是听得人当真了,一个是说的人当真了");

}

语法格式三:数据类型可以省略,因为可由编译器推断得出,称为“类型推断”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void test3(){
//语法格式三使用前
Consumer<String> con1 = (String s) -> {
System.out.println(s);
};
con1.accept("一个是听得人当真了,一个是说的人当真了");

System.out.println("*******************");
//语法格式三使用后
Consumer<String> con2 = (s) -> {
System.out.println(s);
};
con2.accept("一个是听得人当真了,一个是说的人当真了");

}

语法格式四:Lambda 若只需要一个参数时,参数的小括号可以省略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void test4(){
//语法格式四使用前
Consumer<String> con1 = (s) -> {
System.out.println(s);
};
con1.accept("一个是听得人当真了,一个是说的人当真了");

System.out.println("*******************");
//语法格式四使用后
Consumer<String> con2 = s -> {
System.out.println(s);
};
con2.accept("一个是听得人当真了,一个是说的人当真了");


}

语法格式五:Lambda 需要两个或以上的参数,多条执行语句,并且可以有返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Test
public void test5(){
//语法格式五使用前
Comparator<Integer> com1 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
System.out.println(o1);
System.out.println(o2);
return o1.compareTo(o2);
}
};

System.out.println(com1.compare(12,21));
System.out.println("*****************************");
//语法格式五使用后
Comparator<Integer> com2 = (o1,o2) -> {
System.out.println(o1);
System.out.println(o2);
return o1.compareTo(o2);
};

System.out.println(com2.compare(12,6));


}

语法格式六:当 Lambda 体只有一条语句时,return 与大括号若有,都可以省略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Test
public void test6(){
//语法格式六使用前
Comparator<Integer> com1 = (o1,o2) -> {
return o1.compareTo(o2);
};

System.out.println(com1.compare(12,6));

System.out.println("*****************************");
//语法格式六使用后
Comparator<Integer> com2 = (o1,o2) -> o1.compareTo(o2);

System.out.println(com2.compare(12,21));

}

@Test
public void test7(){
//语法格式六使用前
Consumer<String> con1 = s -> {
System.out.println(s);
};
con1.accept("一个是听得人当真了,一个是说的人当真了");

System.out.println("*****************************");
//语法格式六使用后
Consumer<String> con2 = s -> System.out.println(s);

con2.accept("一个是听得人当真了,一个是说的人当真了");

}

Java8新特性:函数式(Functional)接口

什么是函数式接口

  • 只包含一个抽象方法(Single Abstract Method,简称SAM)的接口,称为函数式接口。当然该接口可以包含其他非抽象方法。
  • 你可以通过 Lambda 表达式来创建该接口的对象。(若 Lambda 表达式抛出一个受检异常(即:非运行时异常),那么该异常需要在目标接口的抽象方法上进行声明)。
  • 我们可以在一个接口上使用 @FunctionalInterface 注解,这样做可以检查它是否是一个函数式接口。同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口。
  • java.util.function包下定义了Java 8 的丰富的函数式接口

如何理解函数式接口

  • 简单的说,在Java8中,Lambda表达式就是一个函数式接口的实例。这就是Lambda表达式和函数式接口的关系。也就是说,只要一个对象是函数式接口的实例,那么该对象就可以用Lambda表达式来表示。

Java 内置函数式接口

  • java.lang.Runnable
    • public void run()
  • java.lang.Iterable
    • public Iterator iterate()
  • java.lang.Comparable
    • public int compareTo(T t)
  • java.util.Comparator
    • public int compare(T t1, T t2)
函数式接口 称谓 参数类型 用途
Consumer<T> 消费型接口 T 对类型为T的对象应用操作,包含方法: void accept(T t)
Supplier<T> 供给型接口 返回类型为T的对象,包含方法:T get()
Function<T, R> 函数型接口 T 对类型为T的对象应用操作,并返回结果。结果是R类型的对象。包含方法:R apply(T t)
Predicate<T> 判断型接口 T 确定类型为T的对象是否满足某约束,并返回 boolean 值。包含方法:boolean test(T t)

其他接口:

类型1:消费型接口

消费型接口的抽象方法特点:有形参,但是返回值类型是void

接口名 抽象方法 描述
BiConsumer<T,U> void accept(T t, U u) 接收两个对象用于完成功能
DoubleConsumer void accept(double value) 接收一个double值
IntConsumer void accept(int value) 接收一个int值
LongConsumer void accept(long value) 接收一个long值
ObjDoubleConsumer void accept(T t, double value) 接收一个对象和一个double值
ObjIntConsumer void accept(T t, int value) 接收一个对象和一个int值
ObjLongConsumer void accept(T t, long value) 接收一个对象和一个long值

类型2:供给型接口

这类接口的抽象方法特点:无参,但是有返回值

接口名 抽象方法 描述
BooleanSupplier boolean getAsBoolean() 返回一个boolean值
DoubleSupplier double getAsDouble() 返回一个double值
IntSupplier int getAsInt() 返回一个int值
LongSupplier long getAsLong() 返回一个long值

类型3:函数型接口

这类接口的抽象方法特点:既有参数又有返回值

接口名 抽象方法 描述
UnaryOperator T apply(T t) 接收一个T类型对象,返回一个T类型对象结果
DoubleFunction R apply(double value) 接收一个double值,返回一个R类型对象
IntFunction R apply(int value) 接收一个int值,返回一个R类型对象
LongFunction R apply(long value) 接收一个long值,返回一个R类型对象
ToDoubleFunction double applyAsDouble(T value) 接收一个T类型对象,返回一个double
ToIntFunction int applyAsInt(T value) 接收一个T类型对象,返回一个int
ToLongFunction long applyAsLong(T value) 接收一个T类型对象,返回一个long
DoubleToIntFunction int applyAsInt(double value) 接收一个double值,返回一个int结果
DoubleToLongFunction long applyAsLong(double value) 接收一个double值,返回一个long结果
IntToDoubleFunction double applyAsDouble(int value) 接收一个int值,返回一个double结果
IntToLongFunction long applyAsLong(int value) 接收一个int值,返回一个long结果
LongToDoubleFunction double applyAsDouble(long value) 接收一个long值,返回一个double结果
LongToIntFunction int applyAsInt(long value) 接收一个long值,返回一个int结果
DoubleUnaryOperator double applyAsDouble(double operand) 接收一个double值,返回一个double
IntUnaryOperator int applyAsInt(int operand) 接收一个int值,返回一个int结果
LongUnaryOperator long applyAsLong(long operand) 接收一个long值,返回一个long结果
BiFunction<T,U,R> R apply(T t, U u) 接收一个T类型和一个U类型对象,返回一个R类型对象结果
BinaryOperator T apply(T t, T u) 接收两个T类型对象,返回一个T类型对象结果
ToDoubleBiFunction<T,U> double applyAsDouble(T t, U u) 接收一个T类型和一个U类型对象,返回一个double
ToIntBiFunction<T,U> int applyAsInt(T t, U u) 接收一个T类型和一个U类型对象,返回一个int
ToLongBiFunction<T,U> long applyAsLong(T t, U u) 接收一个T类型和一个U类型对象,返回一个long
DoubleBinaryOperator double applyAsDouble(double left, double right) 接收两个double值,返回一个double结果
IntBinaryOperator int applyAsInt(int left, int right) 接收两个int值,返回一个int结果
LongBinaryOperator long applyAsLong(long left, long right) 接收两个long值,返回一个long结果

类型4:判断型接口

这类接口的抽象方法特点:有参,但是返回值类型是boolean结果。

接口名 抽象方法 描述
BiPredicate<T,U> boolean test(T t, U u) 接收两个对象
DoublePredicate boolean test(double value) 接收一个double值
IntPredicate boolean test(int value) 接收一个int值
LongPredicate boolean test(long value) 接收一个long值

练习

练习1:无参无返回值形式

假如有自定义函数式接口Call如下:

1
2
3
public interface Call {
void shout();
}

在测试类中声明一个如下方法:

1
2
3
public static void callSomething(Call call){
call.shout();
}

在测试类的main方法中调用callSomething方法,并用Lambda表达式为形参call赋值,可以喊出任意你想说的话。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestLambda {
public static void main(String[] args) {
callSomething(()->System.out.println("回家吃饭"));
callSomething(()->System.out.println("我爱你"));
callSomething(()->System.out.println("滚蛋"));
callSomething(()->System.out.println("回来"));
}
public static void callSomething(Call call){
call.shout();
}
}
interface Call {
void shout();
}

练习2:消费型接口

代码示例:Consumer接口

在JDK1.8中Collection集合接口的父接口Iterable接口中增加了一个默认方法:

public default void forEach(Consumer<? super T> action) 遍历Collection集合的每个元素,执行“xxx消费型”操作。

在JDK1.8中Map集合接口中增加了一个默认方法:

public default void forEach(BiConsumer<? super K,? super V> action)遍历Map集合的每对映射关系,执行“xxx消费型”操作。

案例:

(1)创建一个Collection系列的集合,添加一些字符串,调用forEach方法遍历查看

(2)创建一个Map系列的集合,添加一些(key,value)键值对,调用forEach方法遍历查看

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test1(){
List<String> list = Arrays.asList("hello","java","lambda","atguigu");
list.forEach(s -> System.out.println(s));
}
@Test
public void test2(){
HashMap<Integer,String> map = new HashMap<>();
map.put(1, "hello");
map.put(2, "java");
map.put(3, "lambda");
map.put(4, "atguigu");
map.forEach((k,v) -> System.out.println(k+"->"+v));
}

练习3:供给型接口

代码示例:Supplier接口

在JDK1.8中增加了StreamAPI,java.util.stream.Stream是一个数据流。这个类型有一个静态方法:

public static <T> Stream<T> generate(Supplier<T> s)可以创建Stream的对象。而又包含一个forEach方法可以遍历流中的元素:public void forEach(Consumer<? super T> action)

案例:

现在请调用Stream的generate方法,来产生一个流对象,并调用Math.random()方法来产生数据,为Supplier函数式接口的形参赋值。最后调用forEach方法遍历流中的数据查看结果。

1
2
3
4
@Test
public void test2(){
Stream.generate(() -> Math.random()).forEach(num -> System.out.println(num));
}

练习4:功能型接口

代码示例:Function<T,R>接口

在JDK1.8时Map接口增加了很多方法,例如:

public default void replaceAll(BiFunction<? super K,? super V,? extends V> function) 按照function指定的操作替换map中的value。

public default void forEach(BiConsumer<? super K,? super V> action)遍历Map集合的每对映射关系,执行“xxx消费型”操作。

案例:

(1)声明一个Employee员工类型,包含编号、姓名、薪资。

(2)添加n个员工对象到一个HashMap<Integer,Employee>集合中,其中员工编号为key,员工对象为value。

(3)调用Map的forEach遍历集合

(4)调用Map的replaceAll方法,将其中薪资低于10000元的,薪资设置为10000。

(5)再次调用Map的forEach遍历集合查看结果

Employee类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Employee{
private int id;
private String name;
private double salary;
public Employee(int id, String name, double salary) {
super();
this.id = id;
this.name = name;
this.salary = salary;
}
public Employee() {
super();
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
@Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + ", salary=" + salary + "]";
}

}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.util.HashMap;

public class TestLambda {
public static void main(String[] args) {
HashMap<Integer,Employee> map = new HashMap<>();
Employee e1 = new Employee(1, "张三", 8000);
Employee e2 = new Employee(2, "李四", 9000);
Employee e3 = new Employee(3, "王五", 10000);
Employee e4 = new Employee(4, "赵六", 11000);
Employee e5 = new Employee(5, "钱七", 12000);

map.put(e1.getId(), e1);
map.put(e2.getId(), e2);
map.put(e3.getId(), e3);
map.put(e4.getId(), e4);
map.put(e5.getId(), e5);

map.forEach((k,v) -> System.out.println(k+"="+v));
System.out.println();

map.replaceAll((k,v)->{
if(v.getSalary()<10000){
v.setSalary(10000);
}
return v;
});
map.forEach((k,v) -> System.out.println(k+"="+v));
}
}

练习5:判断型接口

代码示例:Predicate接口

JDK1.8时,Collecton接口增加了一下方法,其中一个如下:

public default boolean removeIf(Predicate<? super E> filter) 用于删除集合中满足filter指定的条件判断的。

public default void forEach(Consumer<? super T> action) 遍历Collection集合的每个元素,执行“xxx消费型”操作。

案例:

(1)添加一些字符串到一个Collection集合中

(2)调用forEach遍历集合

(3)调用removeIf方法,删除其中字符串的长度<5的

(4)再次调用forEach遍历集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.ArrayList;

public class TestLambda {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("hello");
list.add("java");
list.add("atguigu");
list.add("ok");
list.add("yes");

list.forEach(str->System.out.println(str));
System.out.println();

list.removeIf(str->str.length()<5);
list.forEach(str->System.out.println(str));
}
}

练习6:判断型接口

案例:

(1)声明一个Employee员工类型,包含编号、姓名、性别,年龄,薪资。

(2)声明一个EmployeeSerice员工管理类,包含一个ArrayList集合的属性all,在EmployeeSerice的构造器中,创建一些员工对象,为all集合初始化。

(3)在EmployeeSerice员工管理类中,声明一个方法:ArrayList get(Predicate p),即将满足p指定的条件的员工,添加到一个新的ArrayList 集合中返回。

(4)在测试类中创建EmployeeSerice员工管理类的对象,并调用get方法,分别获取:

  • 所有员工对象
  • 所有年龄超过35的员工
  • 所有薪资高于15000的女员工
  • 所有编号是偶数的员工
  • 名字是“张三”的员工
  • 年龄超过25,薪资低于10000的男员工

示例代码:

Employee类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class Employee{
private int id;
private String name;
private char gender;
private int age;
private double salary;

public Employee(int id, String name, char gender, int age, double salary) {
super();
this.id = id;
this.name = name;
this.gender = gender;
this.age = age;
this.salary = salary;
}
public Employee() {
super();
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
@Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + ", gender=" + gender + ", age=" + age + ", salary=" + salary
+ "]";
}
}

员工管理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class EmployeeService{
private ArrayList<Employee> all;
public EmployeeService(){
all = new ArrayList<Employee>();
all.add(new Employee(1, "张三", '男', 33, 8000));
all.add(new Employee(2, "翠花", '女', 23, 18000));
all.add(new Employee(3, "无能", '男', 46, 8000));
all.add(new Employee(4, "李四", '女', 23, 9000));
all.add(new Employee(5, "老王", '男', 23, 15000));
all.add(new Employee(6, "大嘴", '男', 23, 11000));
}
public ArrayList<Employee> get(Predicate<Employee> p){
ArrayList<Employee> result = new ArrayList<Employee>();
for (Employee emp : result) {
if(p.test(emp)){
result.add(emp);
}
}
return result;
}
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestLambda {
public static void main(String[] args) {
EmployeeService es = new EmployeeService();

es.get(e -> true).forEach(e->System.out.println(e));
System.out.println();
es.get(e -> e.getAge()>35).forEach(e->System.out.println(e));
System.out.println();
es.get(e -> e.getSalary()>15000 && e.getGender()=='女').forEach(e->System.out.println(e));
System.out.println();
es.get(e -> e.getId()%2==0).forEach(e->System.out.println(e));
System.out.println();
es.get(e -> "张三".equals(e.getName())).forEach(e->System.out.println(e));
System.out.println();
es.get(e -> e.getAge()>25 && e.getSalary()<10000 && e.getGender()=='男').forEach(e->System.out.println(e));
}
}

Java8新特性:方法引用与构造器引用

方法引用

当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用!

方法引用可以看做是Lambda表达式深层次的表达。换句话说,方法引用就是Lambda表达式,也就是函数式接口的一个实例,通过方法的名字来指向一个方法,可以认为是Lambda表达式的一个语法糖。

语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。

方法引用格式
  • 格式:使用方法引用操作符 “::” 将类(或对象) 与 方法名分隔开来。

    • 两个:中间不能有空格,而且必须英文状态下半角输入
  • 如下三种主要使用情况:

    • 情况1:对象 :: 实例方法名
    • 情况2:类 :: 静态方法名
    • 情况3:类 :: 实例方法名
方法引用使用前提

要求1:Lambda体只有一句语句,并且是通过调用一个对象的/类现有的方法来完成的

例如:System.out对象,调用println()方法来完成Lambda体

​ Math类,调用random()静态方法来完成Lambda体

要求2:

针对情况1:函数式接口中的抽象方法a在被重写时使用了某一个对象的方法b。如果方法a的形参列表、返回值类型与方法b的形参列表、返回值类型都相同,则我们可以使用方法b实现对方法a的重写、替换。 对象 :: 实例方法

1
2
3
4
5
6
7
//1
Consumer<String> con1 = str -> System.out.println(str);
Consumer<String> con2 = System.out::println;

//2
Supplier<String> sup1 = () -> emp.getName();
Supplier<String> sup2 = emp::getName;

针对情况2:函数式接口中的抽象方法a在被重写时使用了某一个类的静态方法b。如果方法a的形参列表、返回值类型与方法b的形参列表、返回值类型都相同,则我们可以使用方法b实现对方法a的重写、替换。 类 :: 静态方法

1
2
3
4
5
6
7
//1
Comparator<Integer> com1 = (t1,t2) -> Integer.compare(t1,t2);
Comparator<Integer> com2 = Integer::compare;

//2
Function<Double,Long> func1 = d -> Math.round(d);
Function<Double,Long> func2 = Math::round;

针对情况3:函数式接口中的抽象方法a在被重写时使用了某一个对象的方法b。如果方法a的返回值类型与方法b的返回值类型相同,同时方法a的形参列表中有n个参数,方法b的形参列表有n-1个参数,且方法a的第1个参数作为方法b的调用者,且方法a的后n-1参数与方法b的n-1参数匹配(类型相同或满足多态场景也可以) 类 :: 实例方法

1
2
3
4
5
6
7
8
9
10
11
//1
Comparator<String> com1 = (s1,s2) -> s1.compareTo(s2);
Comparator<String> com2 = String :: compareTo;

//2
BiPredicate<String,String> pre1 = (s1,s2) -> s1.equals(s2);
BiPredicate<String,String> pre2 = String :: equals;

//3
Function<Employee,String> func1 = e -> e.getName();
Function<Employee,String> func2 = Employee::getName;

构造器引用

当Lambda表达式是创建一个对象,并且满足Lambda表达式形参,正好是给创建这个对象的构造器的实参列表,就可以使用构造器引用。

格式:类名::new

1
2
3
4
5
6
7
8
9
10
11
12

Supplier<Employee> sup = new Supplier<Employee>() {
@Override
public Employee get() {
return new Employee();
}
};

Supplier<Employee> sup1 = () -> new Employee();

Supplier<Employee> sup2 = Employee :: new;

数组构造引用

当Lambda表达式是创建一个数组对象,并且满足Lambda表达式形参,正好是给创建这个数组对象的长度,就可以数组构造引用。

格式:数组类型名::new

1
2
Function<Integer,String[]> func1 = length -> new String[length];
Function<Integer,String[]> func2 = String[] :: new;

Java8新特性:强大的Stream API

概述

  • Stream API ( java.util.stream) 把真正的函数式编程风格引入到Java中。这是目前为止对Java类库最好的补充,因为Stream API可以极大提供Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。
  • Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。 使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简言之,Stream API 提供了一种高效且易于使用的处理数据的方式。

为什么要使用Stream API:实际开发中,项目中多数数据源都来自于MySQL、Oracle等。但现在数据源可以更多了,有MongDB,Radis等,而这些NoSQL的数据就需要Java层面去处理。

什么是Stream
Stream 是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。

Stream 和 Collection 集合的区别:Collection 是一种静态的内存数据结构,讲的是数据,而 Stream 是有关计算的,讲的是计算。前者是主要面向内存,存储在内存中,后者主要是面向 CPU,通过 CPU 实现计算。

注意:

①Stream 自己不会存储元素。

②Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。

③Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。即一旦执行终止操作,就执行中间操作链,并产生结果。

④ Stream一旦执行了终止操作,就不能再调用其它中间操作或终止操作了。

Stream的操作三个步骤

1- 创建 Stream
一个数据源(如:集合、数组),获取一个流

2- 中间操作
每次处理都会返回一个持有结果的新Stream,即中间操作的方法返回值仍然是Stream类型的对象。因此中间操作可以是个操作链,可对数据源的数据进行n次处理,但是在终结操作前,并不会真正执行。

3- 终止操作(终端操作)
终止操作的方法返回值类型就不再是Stream了,因此一旦执行终止操作,就结束整个Stream操作了。一旦执行终止操作,就执行中间操作链,最终产生结果并结束Stream。

创建Stream实例

方式一:通过集合

Java8 中的 Collection 接口被扩展,提供了两个获取流的方法:

  • default Stream stream() : 返回一个顺序流

  • default Stream parallelStream() : 返回一个并行流

1
2
3
4
5
6
7
@Test
public void test01(){
List<Integer> list = Arrays.asList(1,2,3,4,5);

//JDK1.8中,Collection系列集合增加了方法
Stream<Integer> stream = list.stream();
}

方式二:通过数组

Java8 中的 Arrays 的静态方法 stream() 可以获取数组流:

  • static Stream stream(T[] array): 返回一个流
  • public static IntStream stream(int[] array)
  • public static LongStream stream(long[] array)
  • public static DoubleStream stream(double[] array)
1
2
3
4
5
6
7
8
9
10
11
@Test
public void test02(){
String[] arr = {"hello","world"};
Stream<String> stream = Arrays.stream(arr);
}

@Test
public void test03(){
int[] arr = {1,2,3,4,5};
IntStream stream = Arrays.stream(arr);
}

方式三:通过Stream的of()

可以调用Stream类静态方法 of(), 通过显示值创建一个流。它可以接收任意数量的参数。

  • public static Stream of(T… values) : 返回一个流
1
2
3
4
5
@Test
public void test04(){
Stream<Integer> stream = Stream.of(1,2,3,4,5);
stream.forEach(System.out::println);
}

方式四:创建无限流(了解)

可以使用静态方法 Stream.iterate() 和 Stream.generate(), 创建无限流。

  • 迭代
    public static Stream iterate(final T seed, final UnaryOperator f)

  • 生成
    public static Stream generate(Supplier s)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 方式四:创建无限流
@Test
public void test05() {
// 迭代
// public static<T> Stream<T> iterate(final T seed, final
// UnaryOperator<T> f)
Stream<Integer> stream = Stream.iterate(0, x -> x + 2);
stream.limit(10).forEach(System.out::println);

// 生成
// public static<T> Stream<T> generate(Supplier<T> s)
Stream<Double> stream1 = Stream.generate(Math::random);
stream1.limit(10).forEach(System.out::println);
}

一系列中间操作

多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值”。

1-筛选与切片

方 法 描 述
filter(Predicatep) 接收 Lambda , 从流中排除某些元素
distinct() 筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
limit(long maxSize) 截断流,使其元素不超过给定数量
skip(long n) 跳过元素,返回一个扔掉了前 n 个元素的流。
若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补

2-映 射

方法 描述
map(Function f) 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
mapToDouble(ToDoubleFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream。
mapToInt(ToIntFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream。
mapToLong(ToLongFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream。
flatMap(Function f) 接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流

3-排序

方法 描述
sorted() 产生一个新流,其中按自然顺序排序
sorted(Comparator com) 产生一个新流,其中按比较器顺序排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99

public class StreamMiddleOperate {
@Test
public void test01(){
//1、创建Stream
Stream<Integer> stream = Stream.of(1,2,3,4,5,6);

//2、加工处理
//过滤:filter(Predicate p)
//把里面的偶数拿出来
/*
* filter(Predicate p)
* Predicate是函数式接口,抽象方法:boolean test(T t)
*/
stream = stream.filter(t -> t%2==0);

//3、终结操作:例如:遍历
stream.forEach(System.out::println);
}
@Test
public void test02(){
Stream.of(1,2,3,4,5,6)
.filter(t -> t%2==0)
.forEach(System.out::println);
}
@Test
public void test03(){
Stream.of(1,2,3,4,5,6,2,2,3,3,4,4,5)
.distinct()
.forEach(System.out::println);
}
@Test
public void test04(){
Stream.of(1,2,3,4,5,6,2,2,3,3,4,4,5)
.limit(3)
.forEach(System.out::println);
}
@Test
public void test05(){
Stream.of(1,2,2,3,3,4,4,5,2,3,4,5,6,7)
.distinct() //(1,2,3,4,5,6,7)
.filter(t -> t%2!=0) //(1,3,5,7)
.limit(3)
.forEach(System.out::println);
}
@Test
public void test06(){
Stream.of(1,2,3,4,5,6,2,2,3,3,4,4,5)
.skip(5)
.forEach(System.out::println);
}
@Test
public void test07(){
Stream.of(1,2,3,4,5,6,2,2,3,3,4,4,5)
.skip(5)
.distinct()
.filter(t -> t%3==0)
.forEach(System.out::println);
}
@Test
public void test08(){
long count = Stream.of(1,2,3,4,5,6,2,2,3,3,4,4,5)
.distinct()
.peek(System.out::println) //Consumer接口的抽象方法 void accept(T t)
.count();
System.out.println("count="+count);
}
@Test
public void test09(){
//希望能够找出前三个最大值,前三名最大的,不重复
Stream.of(11,2,39,4,54,6,2,22,3,3,4,54,54)
.distinct()
.sorted((t1,t2) -> -Integer.compare(t1, t2))//Comparator接口 int compare(T t1, T t2)
.limit(3)
.forEach(System.out::println);
}
@Test
public void test10(){
Stream.of(1,2,3,4,5)
.map(t -> t+=1)//Function<T,R>接口抽象方法 R apply(T t)
.forEach(System.out::println);
}
@Test
public void test11(){
String[] arr = {"hello","world","java"};

Arrays.stream(arr)
.map(t->t.toUpperCase())
.forEach(System.out::println);
}
@Test
public void test12(){
String[] arr = {"hello","world","java"};
Arrays.stream(arr)
.flatMap(t -> Stream.of(t.split("|")))//Function<T,R>接口抽象方法 R apply(T t) 现在的R是一个Stream
.forEach(System.out::println);
}
}

终止操作

终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、Integer,甚至是 void 。

  • 流进行了终止操作后,不能再次使用。

1-匹配与查找

方法 描述
allMatch(Predicate p) 检查是否匹配所有元素
anyMatch(Predicate p) 检查是否至少匹配一个元素
noneMatch(Predicate p) 检查是否没有匹配所有元素
findFirst() 返回第一个元素
findAny() 返回当前流中的任意元素
count() 返回流中元素总数
max(Comparator c) 返回流中最大值
min(Comparator c) 返回流中最小值
forEach(Consumer c) 内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。
相反,Stream API 使用内部迭代——它帮你把迭代做了)

2-归约

方法 描述
reduce(T identity, BinaryOperator b) 可以将流中元素反复结合起来,得到一个值。返回 T
reduce(BinaryOperator b) 可以将流中元素反复结合起来,得到一个值。返回 Optional

备注:map 和 reduce 的连接通常称为 map-reduce 模式,因 Google 用它来进行网络搜索而出名。

3-收集

方 法 描 述
collect(Collector c) 将流转换为其他形式。接收一个 Collector接口的实现,
用于给Stream中元素做汇总的方法

Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、Set、Map)。

另外, Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例,具体方法与实例如下表:

方法 返回类型 作用
toList Collector<T, ?, List> 把流中元素收集到List
1
List<Employee> emps= list.stream().collect(Collectors.toList());
方法 返回类型 作用
toSet Collector<T, ?, Set> 把流中元素收集到Set
1
Set<Employee> emps= list.stream().collect(Collectors.toSet());
方法 返回类型 作用
toCollection Collector<T, ?, C> 把流中元素收集到创建的集合
1
Collection<Employee> emps =list.stream().collect(Collectors.toCollection(ArrayList::new));
方法 返回类型 作用
counting Collector<T, ?, Long> 计算流中元素的个数
1
long count = list.stream().collect(Collectors.counting());
方法 返回类型 作用
summingInt Collector<T, ?, Integer> 对流中元素的整数属性求和
1
int total=list.stream().collect(Collectors.summingInt(Employee::getSalary));
方法 返回类型 作用
averagingInt Collector<T, ?, Double> 计算流中元素Integer属性的平均值
1
double avg = list.stream().collect(Collectors.averagingInt(Employee::getSalary));
方法 返回类型 作用
summarizingInt Collector<T, ?, IntSummaryStatistics> 收集流中Integer属性的统计值。如:平均值
1
int SummaryStatisticsiss= list.stream().collect(Collectors.summarizingInt(Employee::getSalary));
方法 返回类型 作用
joining Collector<CharSequence, ?, String> 连接流中每个字符串
1
String str= list.stream().map(Employee::getName).collect(Collectors.joining());
方法 返回类型 作用
maxBy Collector<T, ?, Optional> 根据比较器选择最大值
1
Optional<Emp>max= list.stream().collect(Collectors.maxBy(comparingInt(Employee::getSalary)));
方法 返回类型 作用
minBy Collector<T, ?, Optional> 根据比较器选择最小值
1
Optional<Emp> min = list.stream().collect(Collectors.minBy(comparingInt(Employee::getSalary)));
方法 返回类型 作用
reducing Collector<T, ?, Optional> 从一个作为累加器的初始值开始,利用BinaryOperator与流中元素逐个结合,从而归约成单个值
1
int total=list.stream().collect(Collectors.reducing(0, Employee::getSalar, Integer::sum));
方法 返回类型 作用
collectingAndThen Collector<T,A,RR> 包裹另一个收集器,对其结果转换函数
1
int how= list.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size));
方法 返回类型 作用
groupingBy Collector<T, ?, Map<K, List>> 根据某属性值对流分组,属性为K,结果为V
1
Map<Emp.Status, List<Emp>> map= list.stream().collect(Collectors.groupingBy(Employee::getStatus));
方法 返回类型 作用
partitioningBy Collector<T, ?, Map<Boolean, List>> 根据true或false进行分区

Java9新增API

新增1:Stream实例化方法

ofNullable()的使用:

Java 8 中 Stream 不能完全为null,否则会报空指针异常。而 Java 9 中的 ofNullable 方法允许我们创建一个单元素 Stream,可以包含一个非空元素,也可以创建一个空 Stream。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//报NullPointerException
//Stream<Object> stream1 = Stream.of(null);
//System.out.println(stream1.count());

//不报异常,允许通过
Stream<String> stringStream = Stream.of("AA", "BB", null);
System.out.println(stringStream.count());//3

//不报异常,允许通过
List<String> list = new ArrayList<>();
list.add("AA");
list.add(null);
System.out.println(list.stream().count());//2
//ofNullable():允许值为null
Stream<Object> stream1 = Stream.ofNullable(null);
System.out.println(stream1.count());//0

Stream<String> stream = Stream.ofNullable("hello world");
System.out.println(stream.count());//1

iterator()重载的使用:

1
2
3
4
5
6
//原来的控制终止方式:
Stream.iterate(1,i -> i + 1).limit(10).forEach(System.out::println);

//现在的终止方式:
Stream.iterate(1,i -> i < 100,i -> i + 1).forEach(System.out::println);

新语法结构

新的语法结构,为我们勾勒出了 Java 语法进化的一个趋势,将开发者从复杂、繁琐的低层次抽象中逐渐解放出来,以更高层次、更优雅的抽象,既降低代码量,又避免意外编程错误的出现,进而提高代码质量和开发效率。

Java的REPL工具: jShell命令

利用jShell在没有创建类的情况下,在命令行里直接声明变量,计算表达式,执行语句。无需跟人解释”public static void main(String[] args)”这句”废话”。

异常处理之try-catch资源关闭

在JDK7 之前,我们这样处理资源的关闭:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Test
public void test01() {
FileWriter fw = null;
BufferedWriter bw = null;
try {
fw = new FileWriter("d:/1.txt");
bw = new BufferedWriter(fw);

bw.write("hello");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bw != null) {
bw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fw != null) {
fw.close();
}
} catch (IOException e) {
e.printStackTrace();
}

}
}

JDK7的新特性

在try的后面可以增加一个(),在括号中可以声明流对象并初始化。try中的代码执行完毕,会自动把流对象释放,就不用写finally了。

格式:

1
2
3
4
5
6
7
try(资源对象的声明和初始化){
业务逻辑代码,可能会产生异常
}catch(异常类型1 e){
处理异常代码
}catch(异常类型2 e){
处理异常代码
}

说明:

1、在try()中声明的资源,无论是否发生异常,无论是否处理异常,都会自动关闭资源对象,不用手动关闭了。

2、这些资源实现类必须实现AutoCloseable或Closeable接口,实现其中的close()方法。Closeable是AutoCloseable的子接口。Java7几乎把所有的“资源类”(包括文件IO的各种类、JDBC编程的Connection、Statement等接口…)都进行了改写,改写后资源类都实现了AutoCloseable或Closeable接口,并实现了close()方法。

3、写到try()中的资源类的变量默认是final声明的,不能修改。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//举例1
@Test
public void test02() {
try (
FileWriter fw = new FileWriter("d:/1.txt");
BufferedWriter bw = new BufferedWriter(fw);
) {
bw.write("hello");
} catch (IOException e) {
e.printStackTrace();
}
}

//举例2
@Test
public void test03() {
//从d:/1.txt(utf-8)文件中,读取内容,写到项目根目录下1.txt(gbk)文件中
try (
FileInputStream fis = new FileInputStream("d:/1.txt");
InputStreamReader isr = new InputStreamReader(fis, "utf-8");
BufferedReader br = new BufferedReader(isr);

FileOutputStream fos = new FileOutputStream("1.txt");
OutputStreamWriter osw = new OutputStreamWriter(fos, "gbk");
BufferedWriter bw = new BufferedWriter(osw);
) {
String str;
while ((str = br.readLine()) != null) {
bw.write(str);
bw.newLine();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

JDK9的新特性

try的前面可以定义流对象,try后面的()中可以直接引用流对象的名称。在try代码执行完毕后,流对象也可以释放掉,也不用写finally了。

格式:

1
2
3
4
5
6
7
A a = new A();
B b = new B();
try(a;b){
可能产生的异常代码
}catch(异常类名 变量名){
异常处理的逻辑
}

举例:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test04() {
InputStreamReader reader = new InputStreamReader(System.in);
OutputStreamWriter writer = new OutputStreamWriter(System.out);
try (reader; writer) {
//reader是final的,不可再被赋值
// reader = null;

} catch (IOException e) {
e.printStackTrace();
}
}

局部变量类型推断

JDK 10的新特性

局部变量的显示类型声明,常常被认为是不必须的,给一个好听的名字反而可以很清楚的表达出下面应该怎样继续。本新特性允许开发人员省略通常不必要的局部变量类型声明,以增强Java语言的体验性、可读性。

  • 使用举例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//1.局部变量的实例化
var list = new ArrayList<String>();

var set = new LinkedHashSet<Integer>();

//2.增强for循环中的索引
for (var v : list) {
System.out.println(v);
}

//3.传统for循环中
for (var i = 0; i < 100; i++) {
System.out.println(i);
}

//4. 返回值类型含复杂泛型结构
var iterator = set.iterator();
//Iterator<Map.Entry<Integer, Student>> iterator = set.iterator();

  • 不适用场景
    • 声明一个成员变量
    • 声明一个数组变量,并为数组静态初始化(省略new的情况下)
    • 方法的返回值类型
    • 方法的参数类型
    • 没有初始化的方法内的局部变量声明
    • 作为catch块中异常类型
    • Lambda表达式中函数式接口的类型
    • 方法引用中函数式接口的类型

注意:

  • var不是一个关键字,而是一个类型名,将它作为变量的类型。不能使用var作为类名。

  • 这不是JavaScript。var并不会改变 Java是一门静态类型语言的事实。编译器负责推断出类型,并把结果写入字节码文件,就好像是开发人员自己敲入类型一样。

instanceof的模式匹配

JDK14中预览特性:

instanceof 模式匹配通过提供更为简便的语法,来提高生产力。有了该功能,可以减少Java程序中显式强制转换的数量,实现更精确、简洁的类型安全的代码。

Java 14之前旧写法:

1
2
3
4
5
6
if(obj instanceof String){
String str = (String)obj; //需要强转
.. str.contains(..)..
}else{
...
}

Java 14新特性写法:

1
2
3
4
5
if(obj instanceof String str){
.. str.contains(..)..
}else{
...
}

JDK15中第二次预览:

没有任何更改。

JDK16中转正特性:

在Java16中转正。

switch表达式

传统switch声明语句的弊端:

  • 匹配是自上而下的,如果忘记写break,后面的case语句不论匹配与否都会执行; —>case穿透
  • 所有的case语句共用一个块范围,在不同的case语句定义的变量名不能重复;
  • 不能在一个case里写多个执行结果一致的条件;
  • 整个switch不能作为表达式返回值;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//常见错误实现
switch(month){
case 3|4|5://3|4|5 用了位运算符,11 | 100 | 101结果是 111是7
System.out.println("春季");
break;
case 6|7|8://6|7|8用了位运算符,110 | 111 | 1000结果是1111是15
System.out.println("夏季");
break;
case 9|10|11://9|10|11用了位运算符,1001 | 1010 | 1011结果是1011是11
System.out.println("秋季");
break;
case 12|1|2://12|1|2 用了位运算符,1100 | 1 | 10 结果是1111,是15
System.out.println("冬季");
break;
default:
System.out.println("输入有误");
}

JDK12中预览特性:

  • Java 12将会对switch声明语句进行扩展,使用case L ->来替代以前的break;,省去了 break 语句,避免了因少写 break 而出错。

  • 同时将多个 case 合并到一行,显得简洁、清晰,也更加优雅的表达逻辑分支。

  • 为了保持兼容性,case 条件语句中依然可以使用字符 : ,但是同一个 switch 结构里不能混用 -> : ,否则编译错误。

1
2
3
4
5
6
7
8
9
10
11
public class SwitchTest1 {
public static void main(String[] args) {
Fruit fruit = Fruit.GRAPE;
switch(fruit){
case PEAR -> System.out.println(4);
case APPLE,MANGO,GRAPE -> System.out.println(5);
case ORANGE,PAPAYA -> System.out.println(6);
default -> throw new IllegalStateException("No Such Fruit:" + fruit);
};
}
}

更进一步:

1
2
3
4
5
6
7
8
9
10
11
12
public class SwitchTest2 {
public static void main(String[] args) {
Fruit fruit = Fruit.GRAPE;
int numberOfLetters = switch(fruit){
case PEAR -> 4;
case APPLE,MANGO,GRAPE -> 5;
case ORANGE,PAPAYA -> 6;
default -> throw new IllegalStateException("No Such Fruit:" + fruit);
};
System.out.println(numberOfLetters);
}
}

JDK13中二次预览特性:

JDK13中引入了yield语句,用于返回值。这意味着,switch表达式(返回值)应该使用yield,switch语句(不返回值)应该使用break。

yield和return的区别在于:return会直接跳出当前循环或者方法,而yield只会跳出当前switch块。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testSwitch3() {
String x = "3";
int i = switch (x) {
case "1":
yield 1;
case "2":
yield 2;
default:
yield 3;
};
System.out.println(i);
}

JDK14中转正特性:

这是JDK 12和JDK 13中的预览特性,现在是正式特性了。

JDK17的预览特性:switch的模式匹配

旧写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
static String formatter(Object o) {
String formatted = "unknown";
if (o instanceof Integer i) {
formatted = String.format("int %d", i);
} else if (o instanceof Long l) {
formatted = String.format("long %d", l);
} else if (o instanceof Double d) {
formatted = String.format("double %f", d);
} else if (o instanceof String s) {
formatted = String.format("String %s", s);
}
return formatted;
}

模式匹配新写法:

1
2
3
4
5
6
7
8
9
static String formatterPatternSwitch(Object o) {
return switch (o) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> o.toString();
};
}

直接在 switch 上支持 Object 类型,这就等于同时支持多种类型,使用模式匹配得到具体类型,大大简化了语法量,这个功能很实用。

文本块

现实问题:

在Java中,通常需要使用String类型表达HTML,XML,SQL或JSON等格式的字符串,在进行字符串赋值时需要进行转义和连接操作,然后才能编译该代码,这种表达方式难以阅读并且难以维护。

JDK13的新特性

使用”””作为文本块的开始符和结束符,在其中就可以放置多行的字符串,不需要进行任何转义。因此,文本块将提高Java程序的可读性和可写性。

基本使用:

1
2
3
4
5
"""
line1
line2
line3
"""

相当于:

1
"line1\nline2\nline3\n"

或者一个连接的字符串:

1
2
3
"line1\n" +
"line2\n" +
"line3\n"

如果字符串末尾不需要行终止符,则结束分隔符可以放在最后一行内容上。例如:

1
2
3
4
"""
line1
line2
line3"""

相当于

1
"line1\nline2\nline3"

文本块可以表示空字符串,但不建议这样做,因为它需要两行源代码:

1
2
String empty = """
""";

JDK14中二次预览特性

JDK14的版本主要增加了两个escape sequences,分别是 \ <line-terminator>\s escape sequence

  • :取消换行操作
  • \s:表示一个空格

JDK15中功能转正

Record

背景

早在2019年2月份,Java 语言架构师 Brian Goetz,曾写文抱怨“Java太啰嗦”或有太多的“繁文缛节”。他提到:开发人员想要创建纯数据载体类(plain data carriers)通常都必须编写大量低价值、重复的、容易出错的代码。如:构造函数、getter/setter、equals()、hashCode()以及toString()等。

以至于很多人选择使用IDE的功能来自动生成这些代码。还有一些开发会选择使用一些第三方类库,如Lombok等来生成这些方法。

JDK14中预览特性:神说要用record,于是就有了。实现一个简单的数据载体类,为了避免编写:构造函数,访问器,equals(),hashCode () ,toString ()等,Java 14推出record。

record 是一种全新的类型,它本质上是一个 final 类,同时所有的属性都是 final 修饰,它会自动编译出 public gethashcodeequalstoString、构造器等结构,减少了代码编写量。

具体来说:当你用record 声明一个类时,该类将自动拥有以下功能:

  • 获取成员变量的简单方法,比如例题中的 name() 和 partner() 。注意区别于我们平常getter()的写法。
  • 一个 equals 方法的实现,执行比较时会比较该类的所有成员属性。
  • 重写 hashCode() 方法。
  • 一个可以打印该类所有成员属性的 toString() 方法。
  • 只有一个构造方法。

此外:

  • 还可以在record声明的类中定义静态字段、静态方法、构造器或实例方法。

  • 不能在record声明的类中定义实例字段;类不能声明为abstract;不能声明显式的父类等。

举例1(新写法):

1
record Point(int x, int y) { }

JDK15中第二次预览特性

JDK16中转正特性

最终到JDK16中转正。

记录不适合哪些场景

record的设计目标是提供一种将数据建模为数据的好方法。它也不是 JavaBeans 的直接替代品,因为record的方法不符合 JavaBeans 的 get 标准。另外 JavaBeans 通常是可变的,而记录是不可变的。尽管它们的用途有点像,但记录并不会以某种方式取代 JavaBean。

密封类

背景:

在 Java 中如果想让一个类不能被继承和修改,这时我们应该使用 final 关键字对类进行修饰。不过这种要么可以继承,要么不能继承的机制不够灵活,有些时候我们可能想让某个类可以被某些类型继承,但是又不能随意继承,是做不到的。Java 15 尝试解决这个问题,引入了 sealed 类,被 sealed 修饰的类可以指定子类。这样这个类就只能被指定的类继承。

JDK15的预览特性:

通过密封的类和接口来限制超类的使用,密封的类和接口限制其它可能继承或实现它们的其它类或接口。

具体使用:

  • 使用修饰符sealed,可以将一个类声明为密封类。密封的类使用保留关键字permits列出可以直接扩展(即extends)它的类。

  • sealed 修饰的类的机制具有传递性,它的子类必须使用指定的关键字进行修饰,且只能是 finalsealednon-sealed 三者之一。

JDK16二次预览特性

JDK17中转正特性

API的变化

Optional 类

JDK8的新特性

到目前为止,臭名昭著的空指针异常是导致Java应用程序失败的最常见原因。以前,为了解决空指针异常,Google在著名的Guava项目引入了Optional类,通过检查空值的方式避免空指针异常。受到Google的启发,Optional类已经成为Java 8类库的一部分。

Optional<T> 类(java.util.Optional) 是一个容器类,它可以保存类型T的值,代表这个值存在。或者仅仅保存null,表示这个值不存在。如果值存在,则isPresent()方法会返回true,调用get()方法会返回该对象。

Optional提供很多有用的方法,这样我们就不用显式进行空值检测。

  • 创建Optional类对象的方法:

  • static Optional empty() :用来创建一个空的Optional实例

    • static Optional of(T value) :用来创建一个Optional实例,value必须非空
    • static <T> Optional<T> ofNullable(T value) :用来创建一个Optional实例,value可能是空,也可能非空
  • 判断Optional容器中是否包含对象:

    • boolean isPresent() : 判断Optional容器中的值是否存在
    • void ifPresent(Consumer<? super T> consumer) :判断Optional容器中的值是否存在,如果存在,就对它进行Consumer指定的操作,如果不存在就不做
  • 获取Optional容器的对象:

  • T get(): 如果调用对象包含值,返回该值。否则抛异常。T get()与of(T value)配合使用

  • T orElse(T other) :orElse(T other) 与ofNullable(T value)配合使用,如果Optional容器中非空,就返回所包装值,如果为空,就用orElse(T other)other指定的默认值(备胎)代替

  • T orElseGet(Supplier<? extends T> other) :如果Optional容器中非空,就返回所包装值,如果为空,就用Supplier接口的Lambda表达式提供的值代替

  • T orElseThrow(Supplier<? extends X> exceptionSupplier) :如果Optional容器中非空,就返回所包装值,如果为空,就抛出你指定的异常类型代替原来的NoSuchElementException

这是JDK9-11的新特性

新增方法 描述 新增的版本
boolean isEmpty() 判断value是否为空 JDK 11
ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) value非空,执行参数1功能;如果value为空,执行参数2功能 JDK 9
Optional or(Supplier<? extends Optional<? extends T>> supplier) value非空,返回对应的Optional;value为空,返回形参封装的Optional JDK 9
Stream stream() value非空,返回仅包含此value的Stream;否则,返回一个空的Stream JDK 9
T orElseThrow() value非空,返回value;否则抛异常NoSuchElementException JDK 10

String存储结构和API变更

这是JDK9的新特性。

产生背景:

Motivation
The current implementation of the String class stores characters in a char array, using two bytes (sixteen bits) for each character. Data gathered from many different applications indicates that strings are a major component of heap usage and, moreover, that most String objects contain only Latin-1 characters. Such characters require only one byte of storage, hence half of the space in the internal char arrays of such String objects is going unused.

使用说明:

Description

We propose to change the internal representation of the String class from a UTF-16 char array to a byte array plus an encoding-flag field. The new String class will store characters encoded either as ISO-8859-1/Latin-1 (one byte per character), or as UTF-16 (two bytes per character), based upon the contents of the string. The encoding flag will indicate which encoding is used.

结论:String 再也不用 char[] 来存储啦,改成了 byte[] 加上编码标记,节约了一些空间。

1
2
3
4
5
6
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
@Stable
private final byte[] value;
...
}

拓展:StringBuffer 与 StringBuilder

那StringBuffer 和 StringBuilder 是否仍无动于衷呢?

String-related classes such as AbstractStringBuilder, StringBuilder, and StringBuffer will be updated to use the same representation, as will the HotSpot VM’s intrinsic string operations.

JDK11新特性:新增了一系列字符串处理方法

描述 举例
判断字符串是否为空白 “ “.isBlank(); // true
去除首尾空白 “ Javastack “.strip(); // “Javastack”
去除尾部空格 “ Javastack “.stripTrailing(); // “ Javastack”
去除首部空格 “ Javastack “.stripLeading(); // “Javastack “
复制字符串 “Java”.repeat(3);// “JavaJavaJava”
行数统计 “A\nB\nC”.lines().count(); // 3

JDK12新特性:String 实现了 Constable 接口

String源码:

1
public final class String implements java.io.Serializable, Comparable<String>, CharSequence,Constable, ConstantDesc {

java.lang.constant.Constable接口定义了抽象方法:

1
2
3
public interface Constable {
Optional<? extends ConstantDesc> describeConstable();
}

Java 12 String 的实现源码:

1
2
3
4
5
6
7
8
9
10
11
/**
* Returns an {@link Optional} containing the nominal descriptor for this
* instance, which is the instance itself.
*
* @return an {@link Optional} describing the {@linkplain String} instance
* @since 12
*/
@Override
public Optional<String> describeConstable() {
return Optional.of(this);
}

很简单,其实就是调用 Optional.of 方法返回一个 Optional 类型。

举例:

1
2
3
4
5
private static void testDescribeConstable() {
String name = "尚硅谷Java高级工程师";
Optional<String> optional = name.describeConstable();
System.out.println(optional.get());
}

结果输出:

1
尚硅谷Java高级工程师

JDK12新特性:String新增方法

String的transform(Function)

1
2
var result = "foo".transform(input -> input + " bar");
System.out.println(result); //foo bar

或者

1
2
var result = "foo".transform(input -> input + " bar").transform(String::toUpperCase)
System.out.println(result); //FOO BAR

对应的源码:

1
2
3
4
5
6
7
8
9
/**
* This method allows the application of a function to {@code this}
* string. The function should expect a single String argument
* and produce an {@code R} result.
* @since 12
*/
public <R> R transform(Function<? super String, ? extends R> f) {
return f.apply(this);
}

在某种情况下,该方法应该被称为map()。

举例:

1
2
3
4
5
6
7
8
9
10
private static void testTransform() {
System.out.println("======test java 12 transform======");
List<String> list1 = List.of("Java", " Python", " C++ ");
List<String> list2 = new ArrayList<>();
list1.forEach(element -> list2.add(element.transform(String::strip)
.transform(String::toUpperCase)
.transform((e) -> "Hi," + e))
);
list2.forEach(System.out::println);
}

结果输出:

1
2
3
4
======test java 12 transform======
Hi,JAVA
Hi,PYTHON
Hi,C++

如果使用Java 8的Stream特性,可以如下实现:

1
2
3
4
5
6
7
8
private static void testTransform1() {
System.out.println("======test before java 12 ======");
List<String> list1 = List.of("Java ", " Python", " C++ ");

Stream<String> stringStream = list1.stream().map(element -> element.strip()).map(String::toUpperCase).map(element -> "Hello," + element);
List<String> list2 = stringStream.collect(Collectors.toList());
list2.forEach(System.out::println);
}

JDK17:标记删除Applet API

Applet API 提供了一种将 Java AWT/Swing 控件嵌入到浏览器网页中的方法。不过,目前 Applet 已经被淘汰。大部分人可能压根就没有用过 Applet。

Applet API 实际上是无用的,因为所有 Web 浏览器供应商都已删除或透露计划放弃对 Java 浏览器插件的支持。Java 9 的时候,Applet API 已经被标记为过时,Java 17 的时候终于标记为删除了。

具体如下:

1
2
3
4
5
6
java.applet.Applet
java.applet.AppletStub
java.applet.AppletContext
java.applet.AudioClip
javax.swing.JApplet
java.beans.AppletInitializer

其它结构变化

JDK9:UnderScore(下划线)使用的限制

在java 8 中,标识符可以独立使用“_”来命名:

1
2
String _ = "hello";
System.out.println(_);

但是,在java 9 中规定“_”不再可以单独命名标识符了,如果使用,会报错

JDK11:更简化的编译运行程序

看下面的代码。

1
2
3
4
5
// 编译
javac JavaStack.java

// 运行
java JavaStack

我们的认知里,要运行一个 Java 源代码必须先编译,再运行。而在 Java 11 版本中,通过一个 java 命令就直接搞定了,如下所示:

1
java JavaStack.java

注意点:

  • 执行源文件中的第一个类,第一个类必须包含主方法。

GC方面新特性

GC是Java主要优势之一。 然而,当GC停顿太长,就会开始影响应用的响应时间。随着现代系统中内存不断增长,用户和程序员希望JVM能够以高效的方式充分利用这些内存, 并且无需长时间的GC暂停时间。

G1 GC

JDK9以后默认的垃圾回收器是G1GC。

JDK10 : 为G1提供并行的Full GC

G1最大的亮点就是可以尽量的避免full gc。但毕竟是“尽量”,在有些情况下,G1就要进行full gc了,比如如果它无法足够快的回收内存的时候,它就会强制停止所有的应用线程然后清理。

在Java10之前,一个单线程版的标记-清除-压缩算法被用于full gc。为了尽量减少full gc带来的影响,在Java10中,就把之前的那个单线程版的标记-清除-压缩的full gc算法改成了支持多个线程同时full gc。这样也算是减少了full gc所带来的停顿,从而提高性能。

你可以通过-XX:ParallelGCThreads参数来指定用于并行GC的线程数。

JDK12:可中断的 G1 Mixed GC

JDK12:增强G1,自动返回未用堆内存给操作系统

Shenandoah GC

JDK12:Shenandoah GC:低停顿时间的GC

image-20220528212254705

Shenandoah 垃圾回收器是 Red Hat 在 2014 年宣布进行的一项垃圾收集器研究项目 Pauseless GC 的实现,旨在针对 JVM 上的内存收回实现低停顿的需求

据 Red Hat 研发 Shenandoah 团队对外宣称,Shenandoah 垃圾回收器的暂停时间与堆大小无关,这意味着无论将堆设置为 200 MB 还是 200 GB,都将拥有一致的系统暂停时间,不过实际使用性能将取决于实际工作堆的大小和工作负载。

Shenandoah GC 主要目标是 99.9% 的暂停小于 10ms,暂停与堆大小无关等。

这是一个实验性功能,不包含在默认(Oracle)的OpenJDK版本中。

Shenandoah开发团队在实际应用中的测试数据:

image-20220528212323729

JDK15:Shenandoah垃圾回收算法转正

Shenandoah垃圾回收算法终于从实验特性转变为产品特性,这是一个从 JDK 12 引入的回收算法,该算法通过与正在运行的 Java 线程同时进行疏散工作来减少 GC 暂停时间。Shenandoah 的暂停时间与堆大小无关,无论堆栈是 200 MB 还是 200 GB,都具有相同的一致暂停时间。

Shenandoah在JDK12被作为experimental引入,在JDK15变为Production;之前需要通过-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC来启用,现在只需要-XX:+UseShenandoahGC即可启用

革命性的 ZGC

JDK11:引入革命性的 ZGC

ZGC,这应该是JDK11最为瞩目的特性,没有之一。

ZGC是一个并发、基于region、压缩型的垃圾收集器。

ZGC的设计目标是:支持TB级内存容量,暂停时间低(<10ms),对整个程序吞吐量的影响小于15%。 将来还可以扩展实现机制,以支持不少令人兴奋的功能,例如多层堆(即热对象置于DRAM和冷对象置于NVMe闪存),或压缩堆。

JDK13:ZGC:将未使用的堆内存归还给操作系统

JDK14:ZGC on macOS和windows

  • JDK14之前,ZGC仅Linux才支持。现在mac或Windows上也能使用ZGC了,示例如下:

    1
    -XX:+UnlockExperimentalVMOptions -XX:+UseZGC
  • ZGC与Shenandoah目标高度相似,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。

image-20220528212414676

image-20220528212421319

JDK15:ZGC 功能转正

ZGC是Java 11引入的新的垃圾收集器,经过了多个实验阶段,自此终于成为正式特性。

但是这并不是替换默认的GC,默认的GC仍然还是G1;之前需要通过-XX:+UnlockExperimentalVMOptions -XX:+UseZGC来启用ZGC,现在只需要-XX:+UseZGC就可以。相信不久的将来它必将成为默认的垃圾回收器。

ZGC的性能已经相当亮眼,用“令人震惊、革命性”来形容,不为过。未来将成为服务端、大内存、低延迟应用的首选垃圾收集器。

怎么形容Shenandoah和ZGC的关系呢?异同点大概如下:

  • 相同点:性能几乎可认为是相同的
  • 不同点:ZGC是Oracle JDK的,根正苗红。而Shenandoah只存在于OpenJDK中,因此使用时需注意你的JDK版本

JDK16:ZGC 并发线程处理

在线程的堆栈处理过程中,总有一个制约因素就是safepoints。在safepoints这个点,Java的线程是要暂停执行的,从而限制了GC的效率。

回顾:

我们都知道,在之前,需要 GC 的时候,为了进行垃圾回收,需要所有的线程都暂停下来,这个暂停的时间我们称为 Stop The World

而为了实现 STW 这个操作, JVM 需要为每个线程选择一个点停止运行,这个点就叫做安全点(Safepoints)

而ZGC的并发线程堆栈处理可以保证Java线程可以在GC safepoints的同时可以并发执行。它有助于提高所开发的Java软件应用程序的性能和效率。

企业真题(十八)

1. JDK新特性的概述

  • 几个重要的版本

    • jdk 5.0 / jdk 8.0 :里程碑式的版本
    • jdk9.0 开始每6个月发布一个新的版本
    • LTS : jdk8 、 jdk 11 、 jdk 17
  • 如何学习新特性

1
2
3
4
5
6
7
> 角度1:新的语法规则 (多关注)
自动装箱、自动拆箱、注解、enum、Lambda表达式、方法引用、switch表达式、try-catch变化、record等

> 角度2:增加、过时、删除API
StringBuilder、ArrayList、新的日期时间的API、Optional等

> 角度3:底层的优化、JVM参数的调整、GC的变化、内存结构(永久代--->元空间)

2. JDK8:lambda表达式

2.1 什么情况下可以使用lambda表达式

  • 在给函数式接口提供实例时,都可以考虑使用lambda表达式。
  • 基本语法的使用(重要)

2.2 函数式接口

  • 常见的函数式接口。Comparator \ Runnable \ java.util.function下定义的丰富的函数式接口

    1
    2
    3
    4
    消费型接口:Consumer<T>     void accept(T t)
    供给型接口:Supplier<T> T get()
    函数型接口:Function<T,R> R apply(T t)
    判断型接口:Predicate<T> boolean test(T t)

2.3 方法引用、构造器引用、数组引用

  • 方法引用、构造器引用、数组引用:看做是lambda表达式的进一步刻画、表达。

  • (掌握)方法引用、构造器引用、数组引用的使用场景。

3. JDK8:Stream API的使用

  • Stream关注于内存中的多个数据的运算。
  • 使用步骤:① Stream 的实例化 ② 一系列的中间操作 ③ 终止操作

4. JDK8之后的新特性:语法层面

  • jShell工具
  • try-catch结构的变化。try(…){ }
  • 局部变量的类型推断:var
  • instanceof的模式匹配
  • switch表达式、switch的模式匹配
  • 文本块的使用:”””文本块”””
  • 新的引用数据类型:record (记录)
  • 密封类:sealed class

5. JDK8之后的新特性:其它

  • Optional类的使用
  • 其他:了解

二、企业真题

JDK8新特性

1. 谈谈java8新特性(京*旗下、时代*宇,信必*、招*信诺,中*外包,金*软件、阿**巴)

1
2
3
类似问题
> JDK1.8相较于JDK1.7有什么不一样?(惠*)
> JDK1.8的新特性有哪些?Stream API + Lambda表达式,还有吗?(久*国际物流)
  • lambda表达式、Stream API
  • jdk7的对比:元空间、HashMap、新的日期时间API、接口变化等。

2. JDK1.8在数据结构上发生了哪些变化 ?(银*数据)

  • 使用元空间替代永久代。 (方法区:jvm规范中提到的结构。
    • HotSpot来讲,jdk7:方法区的落地体现:永久代。 jdk8:方法区的落地体现:元空间。
  • HashMap底层结构

3. 你说的了解 Java的新特性 ,你说说JDK8改进的地方?(银*科技)

4. JDK1.8用的是哪个垃圾回收器?(O**O)

Parallel GC –> jdk9:默认使用G1GC –> ZGC (低延迟)

Lambda表达式

1. Lambda表达式有了解吗,说说如何使用的(O**O)

1
2
类似问题:
> Lambda的理解(国*)

2. 什么是函数式接口?有几种函数式接口(阿**巴)

略。

java.util.function包下定义了丰富的好函数式接口。有4类基础的函数式接口:

消费型接口:Consumer void accept(T t)
供给型接口:Supplier T get()
函数型接口:Function<T,R> R apply(T t)
判断型接口:Predicate boolean test(T t)

Stream API

1. 创建Stream的方式(阿**巴)

三种。

2. 你讲讲stream表达式是咋用的,干啥的?(中*国际,上海**网络)

1
2
3
> Stream API 关注的是多个数据的计算(排序、查找、过滤、映射、遍历等),面向CPU的。
集合关注的数据的存储,面向内存的。
> Stream API 之于集合,类似于SQL之于数据表的查询。

3. 集合用Stream流怎么实现过滤?(润*软件)

filter(Predicate predicate)

4. 用Stream怎么选出List里想要的数据?(惠*)

其它版本新特性

1. 说说JDK15、JDK16、JDK17中的新特性都有什么?(银*数据)

IDEA的日常快捷键和Debug

第1组:通用型

说明 快捷键
复制代码-copy ctrl + c
粘贴-paste ctrl + v
剪切-cut ctrl + x
撤销-undo ctrl + z
反撤销-redo ctrl + shift + z
保存-save all ctrl + s
全选-select all ctrl + a

第2组:提高编写速度(上)

说明 快捷键
智能提示-edit alt + enter
提示代码模板-insert live template ctrl+j
使用xx块环绕-surround with … ctrl+alt+t
调出生成getter/setter/构造器等结构-generate … alt+insert
自动生成返回值变量-introduce variable … ctrl+alt+v
复制指定行的代码-duplicate line or selection ctrl+d
删除指定行的代码-delete line ctrl+y
切换到下一行代码空位-start new line shift + enter
切换到上一行代码空位-start new line before current ctrl +alt+ enter
向上移动代码-move statement up ctrl+shift+↑
向下移动代码-move statement down ctrl+shift+↓
向上移动一行-move line up alt+shift+↑
向下移动一行-move line down alt+shift+↓
方法的形参列表提醒-parameter info ctrl+p

第3组:提高编写速度(下)

说明 快捷键
批量修改指定的变量名、方法名、类名等-rename shift+f6
抽取代码重构方法-extract method … ctrl+alt+m
重写父类的方法-override methods … ctrl+o
实现接口的方法-implements methods … ctrl+i
选中的结构的大小写的切换-toggle case ctrl+shift+u
批量导包-optimize imports ctrl+alt+o

第4组:类结构、查找和查看源码

说明 快捷键
如何查看源码-go to class… ctrl + 选中指定的结构 或 ctrl+n
显示当前类结构,支持搜索指定的方法、属性等-file structure ctrl+f12
退回到前一个编辑的页面-back ctrl+alt+←
进入到下一个编辑的页面-forward ctrl+alt+→
打开的类文件之间切换-select previous/next tab alt+←/→
光标选中指定的类,查看继承树结构-Type Hierarchy ctrl+h
查看方法文档-quick documentation ctrl+q
类的UML关系图-show uml popup ctrl+alt+u
定位某行-go to line/column ctrl+g
回溯变量或方法的来源-go to implementation(s) ctrl+alt+b
折叠方法实现-collapse all ctrl+shift+ -
展开方法实现-expand all ctrl+shift+ +

第5组:查找、替换与关闭

说明 快捷键
查找指定的结构 ctlr+f
快速查找:选中的Word快速定位到下一个-find next ctrl+l
查找与替换-replace ctrl+r
直接定位到当前行的首位-move caret to line start home
直接定位到当前行的末位 -move caret to line end end
查询当前元素在当前文件中的引用,然后按 F3 可以选择 ctrl+f7
全项目搜索文本-find in path … ctrl+shift+f
关闭当前窗口-close ctrl+f4

第6组:调整格式

说明 快捷键
格式化代码-reformat code ctrl+alt+l
使用单行注释-comment with line comment ctrl + /
使用/取消多行注释-comment with block comment ctrl + shift + /
选中数行,整体往后移动-tab tab
选中数行,整体往前移动-prev tab shift + tab

Debug快捷键

说明 快捷键
单步调试(不进入函数内部)- step over F8
单步调试(进入函数内部)- step into F7
强制单步调试(进入函数内部) - force step into alt+shift+f7
选择要进入的函数 - smart step into shift + F7
跳出函数 - step out shift + F8
运行到断点 - run to cursor alt + F9
继续执行,进入下一个断点或执行完程序 - resume program F9
停止 - stop Ctrl+F2
查看断点 - view breakpoints Ctrl+Shift+F8
关闭 - close Ctrl+F4

面试题整理

offer---JUC

JAVA并发编程—JUC-基础篇 (java.util.concurrent)

JUC概述

  • 进程与线程的区别
  • 线程的状态(New新建,Runnable准备就绪,Blocked阻塞,Waiting不见不散,Timed_waiting过时不候)
  • wait和sleep的区别:
    1. sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用。
    2. sleep不会释放锁,它也不需要占用锁。wait会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)。
    3. 它们都可以被interrupted方法中断。
  • 并发和并行的区别:同一时刻多个线程在访问同一个资源,多个线程对一个点(春运抢票);多项工作一起执行,之后再汇总(泡面的例子)。
  • 管程、用户线程和守护线程

管程

Monitor监视器,锁,是一种同步机制,保证同一个时间,只有一个线程访问被保护数据或者代码。JVM同步基于进入和退出,使用管程对象实现的。

用户线程和守护线程

  • 用户线程:自定义线程(主线程结束了,用户线程还在运行,JVM存活)
  • 守护线程:默默执行在后台的线程,比如垃圾回收(没有用户线程了,都是守护线程,JVM结束)

Lock接口

Synchronized关键字

同步锁。

  1. 修饰一个代码块;
  2. 修饰一个方法;
  3. 修饰一个静态方法;
  4. 修改一个类。

多线程编程的步骤:1. 创建资源类,在资源类中创建属性和操作方法;2. 在资源类操作方法,判断,干活,通知;3. 创建多个线程,调用资源类的操作方法;

卖票示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Ticket {
private int number = 30;
public synchronized void sale(){
if (number > 0){
System.out.println(Thread.currentThread().getName() + ": 卖出 :" + (number--) + "剩下:" + number);
}
}

}

public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}
},"aa").start();

new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}
},"bb").start();

new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}
},"cc").start();
}
}

什么是Lock接口

手动实现上锁、释放锁。

  • 可重入锁
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    class LockTicket {
    private int number = 30;

    //创建可重入锁
    private final ReentrantLock lock = new ReentrantLock();

    public void sale() {
    //上锁
    lock.lock();
    try {
    if (number > 0) {
    System.out.println(Thread.currentThread().getName() + ": 卖出 :" + (number--) + "剩下:" + number);
    }
    }finally {
    //解锁
    lock.unlock();
    }
    }
    }

Lock和synchronized的不同:

  1. Lock是一个接口,而synchronized是Java中的关键字;
  2. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unlock()去释放锁,则很可能造成死锁现象,这也是为什么需要在finally块中释放锁;
  3. Lock可以让等待锁的线程响应中断,而synchronized缺不行,等待的线程会一直等待下去,不能够响应中断;
  4. 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到;
  5. Lock可以提高多个线程进行读操作的效率。

进程间通信

  1. 判断;
  2. 干活;
  3. 通知;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

class Share {
private int number = 0;
public synchronized void incr() throws InterruptedException {
//判断 if改成while
if (number != 0){
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+ ":" +number);
//通知其他线程
this.notifyAll();
}
public synchronized void decr() throws InterruptedException {
//判断
if (number != 1){
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+ ":" +number);
//通知其他线程
this.notifyAll();
}
}

public class ThreadTest {
public static void main(String[] args) {
Share share = new Share();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"INCR").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"DECR").start();
}
}

  • 虚假唤醒问题:wait()方法特点,在哪里睡,在哪里醒,就会导致if判断失效。所以解决方法是把if改成while。

Lock实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

class LockShare{
private int number = 0;
private final ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void incr() throws InterruptedException {
lock.lock();
try {
while (number!=0){
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName()+ ":" +number);
condition.signalAll();
}finally {
lock.unlock();
}
}
public void decr() throws InterruptedException {
lock.lock();
try {
while (number!=1){
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName()+ ":" +number);
condition.signalAll();
}finally {
lock.unlock();
}
}
}


  • 进程间定制化通信:启动三个线程,按照“AA打印5次,BB打印10次,CC打印15次”进行十轮
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

class CustomShare{
private int flag = 1;
private Lock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();

public void print5(int loop) throws InterruptedException {
lock.lock();
try {
while (flag != 1) {
conditionA.await();
}
for (int i = 0; i < 5 ; i++) {
System.out.println(Thread.currentThread().getName() + ":" + (i+1) + ":轮数:" + loop);
}
flag = 2;
conditionA.signal();
}finally {
lock.unlock();
}
}

public void print10(int loop) throws InterruptedException {
lock.lock();
try {
while (flag != 2) {
conditionA.await();
}
for (int i = 0; i < 10 ; i++) {
System.out.println(Thread.currentThread().getName() + ":" + (i+1) + ":轮数:" + loop);
}
flag = 3;
conditionA.signal();
}finally {
lock.unlock();
}
}

public void print15(int loop) throws InterruptedException {
lock.lock();
try {
while (flag != 3) {
conditionA.await();
}
for (int i = 0; i < 15 ; i++) {
System.out.println(Thread.currentThread().getName() + ":" + (i+1) + ":轮数:" + loop);
}
flag = 1;
conditionA.signal();
}finally {
lock.unlock();
}
}

}

public class LockCustomThread {
public static void main(String[] args) {
CustomShare customShare = new CustomShare();
new Thread(()->{
try {
customShare.print5(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"AA").start();
new Thread(()->{
try {
customShare.print10(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"BB").start();
new Thread(()->{
try {
customShare.print15(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"CC").start();
}
}

集合线程安全

  1. List

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13

public class ListThread {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8)); // 报错位置
System.out.println(list);
},String.valueOf(i)).start();
}
}
}

  • 报异常:java.util.ConcurrentModificationException

  • 原因是List.add()方法没有synchronized关键字修饰

  • 解决方案(一):vector
    List list = new Vector<>(); // JDK 1.0 Vector类中的方法有synchronized关键字修饰

  • 解决方案(二):Collection工具类
    List list = Collections.synchronizedList(new ArrayList<>()); // Collections.synchronizedList返回同步列表

  • 解决方案(三):CopyOnWriteArrayList
    List list = new CopyOnWriteArrayList(); // 写时复制技术,并发读、复制一份新内容,独立写

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
        
    public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    Object[] elements = getArray();
    int len = elements.length;
    Object[] newElements = Arrays.copyOf(elements, len + 1);
    newElements[len] = e;
    setArray(newElements);
    return true;
    } finally {
    lock.unlock();
    }
    }

  1. HashSet
    解决方案:CopyOnWriteArraySet

  2. HashMap
    解决方案:ConcurrentHashMap

多线程锁

锁的范围

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// 对应8
class Phone {
public static synchronized void sendSMS() throws Exception {
TimeUnit.SECONDS.sleep(4);
System.out.println("====endSMS");
}

public synchronized void sendEmail() throws Exception {
System.out.println("====endEmail");
}

public void getHello() {
System.out.println("====getHello");
}
}

public class EmailSMSThread {
public static void main(String[] args) throws InterruptedException {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
try {
phone1.sendSMS();;
}catch (Exception e){
e.printStackTrace();
}
},"AA").start();

Thread.sleep(100);

new Thread(()->{
try {
phone2.sendEmail();
}catch (Exception e){
e.printStackTrace();
}
},"BB").start();
}
}

/**
* 8锁
1. 标准访问,先打印短信还是邮件;
====endSMS
====endEmail

2. 停4秒在短信方法内,先打印短信还是邮件;
====endSMS
====endEmail

3. 新增普通的hello方法,先打印短信还是hello;
====getHello
====endSMS

4. 现在有两部手机,先打短信还是邮件;
====endEmail
====endSMS

5. 两个静态同步方法,一部手机,先打印短信还是邮件;
====endSMS
====endEmail

6. 两个静态同步方法,两部手机,先打印短信还是邮件;
====endSMS
====endEmail

7. 一个静态同步方法,一个普通同步方法,一部手机,先打印短信还是邮件;
====endEmail
====endSMS


8. 一个静态同步方法,一个普通同步方法,两部手机,先打印短信还是邮件;
====endEmail
====endSMS

*/
  • 1、2锁的是当前对象;3新增的普通方法与锁无关,所以先执行;4有两个对象,两把锁,各锁各的;5、6锁的范围是类Class;7、8一个锁的是对象,一个锁的是类。
  • 总结:synchronized实现同步的基础是Java中的每一个对象都可以作为锁。
    1. 对于普通同步方法,锁是当前实例对象。
    2. 对于静态同步方法,锁是当前类的Class对象。
    3. 对于同步方法块,锁是Synchronized括号里配置的对象。

公平锁和非公平锁

卖票案例中,三个线程,可能只有一个线程卖了全部的票,导致其他线程饿死,这是非公平锁的情况。

  • 非公平锁:线程饿死、效率高; 公平锁:阳光普照、效率相对低。

可重入锁

  • 又叫递归锁,synchronized(隐式)和Lock(显式)都是可重入锁(一把锁可进入各个区域)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

public class SyncLock {
public synchronized void add(){
add();
}

public static void main(String[] args) {
Object o = new Object();
new Thread(()->{
synchronized (o){
System.out.println(Thread.currentThread().getName()+"外层");
synchronized (o){
System.out.println(Thread.currentThread().getName()+"中层");
synchronized (o){
System.out.println(Thread.currentThread().getName()+"内层");
}
}
}
},"AA").start();

}
}

// 输出: 外层、中层、内层

死锁

  • 两个或者两个以上进程在执行过程中,因为争夺资源而造成一种互相等待的现象。如果没有外力干涉,它们无法再执行下去。
  • 原因:1. 系统资源不足 2. 进程运行推进顺序不合适 3. 资源分配不当
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

public class DeadLock {
static Object a = new Object();
static Object b = new Object();

public static void main(String[] args) {
new Thread(()->{
synchronized (a){
System.out.println(Thread.currentThread().getName()+"持有锁a,试图获取锁b");
synchronized (b){
System.out.println(Thread.currentThread().getName()+"持有锁b");
}
}
},"A").start();
new Thread(()->{
synchronized (b){
System.out.println(Thread.currentThread().getName()+"持有锁b,试图获取锁a");
synchronized (a){
System.out.println(Thread.currentThread().getName()+"持有锁a");
}
}
},"B").start();
}
}

  • 验证是否是死锁
    1. jps -l类似linux ps -ef;先获取当前运行程序的进程号
    2. jstack jvm自带的堆栈跟踪工具,根据进程号查询。

Callable接口

创建线程的多种方式

  1. 继承Thread类;
  2. 实现Runnable接口;
  3. 实现Callable接口;
  4. 线程池的方式。

Runnable和Callable接口的区别

(1)是否有返回值:无;有
(2)是否抛出异常:无;有
(3)实现方法名称:run();call()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

class MyThread1 implements Runnable{
@Override
public void run() {

}
}

class MyThread2 implements Callable {

@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "come in callable");
return 20;
}
}

public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// Runnable接口创建
new Thread(new MyThread1(),"AA").start();

// Callable接口,报错
// new Thread(new MyThread2(),"BB").start();

// FutureTask:一个可取消的异步计算
FutureTask<Integer> futureTask1 = new FutureTask<>(new MyThread2());

FutureTask<Integer> futureTask2 = new FutureTask<>(()->{
System.out.println(Thread.currentThread().getName() + "come in callable");
return 1024;
});

//创建一个线程
new Thread(futureTask1,"jack").start();
new Thread(futureTask2,"lucy").start();
System.out.println(futureTask1.get());
System.out.println(futureTask2.get());
}
}

辅助类

CountDownLatch

  • 减法计数器;countDown()用来减一,该线程不会阻塞;当一个或多个线程调用await()方法时,这些线程会阻塞;当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。

  • 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

class CountDownDemo {
// 六个同学陆续离开教室之后,班长才可以锁门
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch =new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 同学离开了教室");
countDownLatch.countDown();
},String.valueOf(i)).start();

}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"班长锁门走人了");
}
}

CyclicBarrier

  • 循环栅栏,加法计数器
  • 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class CyclicBarrierDemo {
//创建固定值
public static final int NUMBER = 7;
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER,()->{
System.out.println("集齐7颗,召唤神龙");
});

for (int i = 1; i <= 7; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"星龙被收集了");
//等待
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}


Semaphore

  • 计数信号量;信号量维护了一个许可集,如有必要,在许可可用前会阻塞每一个acquire(),然后再获取该许可。每个release()添加一个许可,从而可能释放一个正在阻塞的获取者。一个场景就是控制并发量。
  • 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

public class SemaphoreDemo {
// 6辆车,停3个车位
public static void main(String[] args) {
//创建Semaphore,设置许可数量
Semaphore semaphore = new Semaphore(3);
// 6辆车
for (int i = 1; i <= 6; i++) {
new Thread(()->{
try {
//抢占
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "抢到了车位");
//设置随机停车时间
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName() + "离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放
semaphore.release();
}

},String.valueOf(i)).start();
}
}
}

读写锁

悲观锁和乐观锁

悲观锁:不支持并发操作,频繁地上锁、释放锁,效率低。
乐观锁:支持并发,版本号控制,适用于多读的应用类型,这样可以提高吞吐量。

  • 乐观锁的缺点:
  1. ABA 问题

    如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 “ABA”问题。

    JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

  2. 循环时间长开销大

    自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

  3. 只能保证一个共享变量的原子操作

    CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

表锁和行锁

  1. 行锁(Row Lock):
    a. 锁定范围: 行锁是对表中的一行数据进行锁定,而不是锁定整个表。这意味着其他事务仍然可以访问表中的其他行,不受锁定行的影响。
    b. 适用场景: 适用于高并发读写的情况,允许多个事务同时访问表的不同行,降低了锁的争用。

  2. 表锁(Table Lock):
    a. 锁定范围: 表锁是对整个表进行锁定,当一个事务获取了对表的锁时,其他事务无法同时访问该表,即使它们要访问的是不同的行。
    b. 适用场景: 适用于需要保证整个表的一致性的场景,例如在对整个表进行大批量更新或者维护操作时。

  3. 粒度:
    a. 行锁: 锁定的粒度更细,只影响到实际需要修改的行,不会对表的其他部分产生影响。
    b. 表锁: 锁定的粒度更大,会阻塞对整个表的访问,可能导致并发性能下降。

  4. 性能:
    a. 行锁: 在高并发读写的场景中性能较好,因为允许多个事务同时访问表的不同行。
    b. 表锁: 在高并发写入的场景中可能会导致性能问题,因为需要等待对整个表的锁释放。

  5. 死锁风险:
    a. 行锁: 会死锁;锁定粒度小,发生锁冲突的概率较低。
    b. 表锁: 无死锁;锁定粒度大,发生锁冲突的概率最高。

读锁和写锁

  • 读锁:共享锁,会死锁。如1修改要等2读之后,2修改要等1读之后。
  • 写锁:独占锁,会死锁。
    读写锁:一个资源可以被多个读线程访问,或者可以被一个写线程访问,但是不能同时存在读写进程,读写互斥,读读共享的。
  1. 无锁,多线程抢断资源;
  2. 添加锁,使用synchronized和ReentrantLock,都是独占的,每次只能来一个操作;
  3. 读写锁,读读可以共享,提升性能,同时多人进行读操作,缺点(1造成锁饥饿,一直读没有写;2读时候,不能写,只有读完成之后,才可以写,写操作可以读)
  • 锁降级:将写入锁降级为读锁,读锁不能升级为写锁

读写锁案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

class MyCache{
private volatile Map<String, Object> map = new HashMap<>();

private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 放数据
public void put(String key,Object value){
readWriteLock.writeLock().lock();
System.out.println(Thread.currentThread().getName()+"正在写操作" +key);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"写完了" +key);
readWriteLock.writeLock().unlock();
}

// 取数据
public Object get(String key){
readWriteLock.readLock().lock();
Object result = null;
System.out.println(Thread.currentThread().getName()+"正在读操作" +key);
result = map.get(key);
System.out.println(Thread.currentThread().getName()+"读完了" +key);
readWriteLock.readLock().unlock();
return result;
}

}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(()->{
myCache.put(num+"",num+"");
},String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(()->{
myCache.get(num+"");
},String.valueOf(i)).start();
}
}
}

阻塞队列

通过一个共享的队列,使数据由队列的一端输入,从另外一端输出;当队列空时,获取元素的线程会被阻塞;当队列满时,添加元素的线程会被阻塞。

为什么需要BlockingQueue? 好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,BlockingQueue一手包办。

BlockingQueue的实现类:

  1. ArrayBlockingQueue 由数组结构组成的有界阻塞队列
  2. LinkedBlockingQueue 由链表结构组成的有界(integer.MAX_VALUE)阻塞队列
  3. DelayQueue 使用优先级队列实现的延迟无界阻塞队列
  4. PriorityBlockingQueue 支持优先级排序的无界阻塞队列
  5. SynchronousQueue 不存储元素的阻塞队列,也即单个元素的队列
  6. LinkedTransferQueue 由链表结构组成的无界阻塞队列
  7. LinkedBlockingDeque 由链表结构组成的双向阻塞队列

核心方法:

线程池

优势:线程池做的工作只是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
特点:

  1. 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
  2. 提高响应速度:当任务到达时,任务可以不需要等待线程创建就能立即执行;
  3. 提高线程的可管理性:利用线程池进行统一的分配,调优和监控。

线程池的使用方式

  1. Executors.newFixedThreadPool(int) 一池N线程
  2. Executors.newSingleThreadExecutor() 一个任务一个任务执行,一池一线程
  3. Executors.newCachedThreadPool() 线程池根据需求创建线程,可扩容

ThreadPoolExecutor的七个参数

  • int corePoolSize 常驻线程数量
  • int maximumPoolSize 最大线程数量
  • long KeepAliveTime, TimeUnit unit 线程存活时间
  • BlockingQueue workQueue 阻塞队列
  • ThreadFactory threadFactory 线程工厂
  • RejectedExecutionHandler handle 拒绝策略

线程池的工作流程:

JDK内置的拒绝策略:

自定义线程池

一般情况下,不允许使用Executors创建线程池,而是通过自定义ThreadPoolExecutor的方式,以规避资源耗尽的风险。

1
2
3
4
5
6
7
8
9
10
11

ExecutorService threadPool = new ThreadPoolExecutor(
2,5,2L,TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
threadPool.execute(()->{
...
});

Fork/Join

Fork:把一个复杂任务进行分拆,大事化小
Join:把分拆任务的结果进行合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

class ForkJoinSumCalculate extends RecursiveTask<Long>{

private static final long serialVersionUID = -259195479995561737L;

private long start;
private long end;

private static final long THURSHOLD = 10000L; //临界值

public ForkJoinSumCalculate(long start, long end) {
this.start = start;
this.end = end;
}

@Override
protected Long compute() {
long length = end - start;

if(length <= THURSHOLD){
long sum = 0L;

for (long i = start; i <= end; i++) {
sum += i;
}

return sum;
}else{
long middle = (start + end) / 2;

ForkJoinSumCalculate left = new ForkJoinSumCalculate(start, middle);
left.fork(); //进行拆分,同时压入线程队列

ForkJoinSumCalculate right = new ForkJoinSumCalculate(middle+1, end);
right.fork(); //

return left.join() + right.join();
}
}

}


异步回调

CompletableFuture

  1. 无返回值的异步调用

    1
    2
    3
    4
    5
    6
    7

    CompletableFuture<Void> cf = CompletableFuture.runAsync(()->{
    // ...
    });

    cf.get();

  2. 有返回值的异步调用

1
2
3
4
5
6
7
8
9

CompletableFuture<Integer> cf = CompletableFuture.supplyAsync(()->{
return 1024;
});

cf.whenComplete((u,t)->{
// u 返回值;t 异常信息
}).get();

面试题

1. 并发和并行

并发:一台机器上“同时”处理多个任务,但同一时刻只有一个在发生;
并行:在同一时刻,在多台处理器上同时处理多个任务。

2. 进程、线程、管程

进程:应用程序的一次执行过程,动态的,包括进程从创建、运行和消亡的过程。系统运行程序的基本单元。
线程:轻量级线程。同类的线程共享进程的堆和方法区,每个线程有自己的程序计数器、虚拟机栈和本地方法栈;操作系统调度的基本单元。
管程:Monitor,锁,一种同步机制。JVM同步基于管程,底层由C++实现。当JVM对象被用作同步锁时,JVM会为该对象关联一个Monitor;而该对象不再被用作同步锁或对象被垃圾回收时,Monitor可能会被JVM内部释放或重新利用。

3. 线程的分类

1) 用户线程;2)守护线程:为其他线程服务的。thread.setDaemon(true)。没有用户线程,JVM结束。

4. Future接口(FutureTask实现类) —JDK5

特点:多线程,有返回值,异步任务
优点:和线程池异步多线程任务配合使用效率高;
缺点:get() 阻塞;isDone()轮询耗费CPU资源。

5. CompletableFuture —JDK8

  • 出现的原因:1)针对Future的缺点;2)传入回调参数,实现复杂功能,以观察者模式。
  • 接口CompletionStage和类CompletableFuture
  • 静态构造方法:1)runAsyn无返回值;2)supplyAsyn有返回值。
  • Executor参数说明:若未指定,则使用默认的ForkJoinPoolCommonPool();自定义线程池为非守护线程,所以就会继续执行。用ForkJoinPool是守护线程,可能会出现main线程结束后,JVM也结束了。
  • CompletableFuture可传入回调对象,当异步任务完成或发生异常时,自动调用回调对象。

6. 函数式接口

接口名称 方法名称 参数 返回值
Runnable run
Function apply 1
Consume accept 1
Supplier get
BiConsumer accept 2

7. CompletableFuture常用方法

  1. 获取结果和主动触发计算:

    • get();
    • get(long timeout, TimeUnit unit);
    • join() 和get一样用,只是不抛出异常;
    • getNow(T valuelfAbsent) —>计算完成就返回正常值,否则返回备胎值(传入的参数),立即获取结果不阻塞
    • complete(T value) —->是否打断get方法立即返回括号值
  2. 对计算结果进行处理:

    • thenApply:计算结果存在依赖关系,串行化,有异常叫停
    • handle:有异常也往下走
    • thenAccept:接受任务的处理结果,并消费处理,无返回结果

    如果执行第一个任务传入了自定义线程池,thenRun执行第二个任务,则共用同一个线程池;若用thenRunAsync则各用各的。
    原因:带Async的方法底层调用uniRunStage(asyncPool, action)更改线程池为默认的,而不是自定义的。

  3. 对计算速度进行选用:
    playA.applyToEither(playB, f -> {
    return f + “ is winner”;
    });

  4. 对计算结果进行了合并:
    两个CompletableStage任务都完成后,最终能把两个任务的结果一起交给thenCombine来处理;先完成的先等着,等待其他分支任务。

8. 悲观锁和乐观锁

悲观锁:synchronized和Lock的实现类,适合写操作多的场景;
乐观锁:版本号机制Version,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。适合读操作多的场景。

9. synchronized线程8锁问题

  • 对于普通同步方法,锁的是当前实例对象,通常指this,所有的同步方法用的都是同一把锁—>实例对象本身
  • 对于静态同步方法,锁的时当前类的Class对象
  • 对于同步方法块,锁的时synchronized括号内的对象

10. 从字节码角度分析synchronized实现

  • synchronized同步代码块:实现使用的是monitorenter和monitorexit指令;一般是一个enter两个exit,一个正常情况退出锁,一个异常情况退出锁。
  • synchronized普通同步方法:ACC_SYNCHRONIZED访问标志。
  • synchronized静态同步方法:ACC_STATIC、ACC_SYNCHRONIZED访问标志。

为什么任何一个对象都可以成为一个锁?
C++源码:ObjectMonitor.java—>ObjectMonitor.cpp—>ObjectMonitor.hpp
每个对象天生都带着一个对象监视器,每一个被锁住的对象都会和Monitor关联起来。

11. 公平锁与非公平锁

  1. 为什么会有公平锁/非公平锁的设计?为什么默认非公平?
    非公平锁能更充分地利用CPU的时间片,尽量减少CPU空闲状态时间。减少线程切换的开销。可能产生线程饥饿。

  2. 什么时候用公平?什么时候用非公平?
    如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省了很多线程切换的时间,吞吐量自然就上去了;否则就用公平锁,大家公平使用。

12. 可重入锁

  1. 隐式锁(即synchronized关键字使用的锁);
  2. 显式锁(即Lock)也有ReentrantLock这样的可重入锁;
    Objectmonitor底层会维护一个计数器每lock一次就+1,每unlock一次-1,0表示没有线程占用。(因此lock和unlock要成对出现)

13. 死锁及排查

死锁产生原因:
- 系统资源不足
- 进程运行推进顺序不合适
- 系统资源分配不当

排查:

  1. jps -l + jstack 进程编号
  2. jconsole图形化

14. 线程中断

  • 一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止,所以,Thread.stop,Thread.suspend,Thread.resume都已经被废弃了
  • Java中没有办法立即停止一条线程,Java提供了一种用于停止线程的协商机制—-中断,中断的过程完全需要程序员自行实现。

三个方法:

  1. public void interrupt() 仅仅是设置线程的中断状态为true,发起一个协商而不会立刻停止线程
  2. public static boolean interrupted() 判断线程是否被中断并清除当前中断状态,重新设置为false
  3. public boolean isInterrupted() 判断当前线程是否被中断(通过检查中断标志位)

如何停止终端运行中的线程?

  1. 通过一个volatile变量实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    new Thread(() -> {
    while (true) {
    if (isStop) {
    System.out.println(Thread.currentThread().getName() + " isStop的值被改为true,t1程序停止");
    break;
    }
    System.out.println("-----------hello volatile");
    }
    }, "t1").start();

  2. 通过AutomicBoolean

  3. 通过Thread类自带的中断API实例方法实现—-在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑stop线程。

当前线程的中断标识为true,是不是线程就立刻停止?

  • 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true,仅此而已,被设置中断标志的线程将继续正常运行,不受影响,所以interrupt()并不能真正的中断线程,需要被调用的线程自己进行配合才行,对于不活动的线程没有任何影响。
  • 如果线程处于阻塞状态(例如sleep,wait,join状态等),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态(interrupt状态也将被清除),并抛出一个InterruptedException异常。

静态方法Thread.interrupted(),谈谈你的理解?
中断标识被清空,如果该方法连续被调用两次,第二次调用将返回false;除非当前线程在第一次调用和第二次调用该方法之间再次被interrupt。

15. LockSupport

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语,其中park()和unpack()而作用分别是阻塞线程和解除阻塞线程。

三种让线程等待和唤醒的方法

  1. 使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
  2. 使用JUC包中的Condition的await()方法让线程等待,使用signal()方法唤醒线程
  3. LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

LockSupport类中的park等待和unpark唤醒

  • LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(Permit),许可证只能有一个,累加上限是1。
  • park/park(Object blocker)——-阻塞当前线程/阻塞传入的具体线程;
  • unpark(Thread thread)——唤醒处于阻塞状态的指定线程

为什么要有LockSupport

  1. wait和notify方法必须要在同步代码块或者方法里面,且成对出现使用,先wait再notify才ok。
  2. Condition中的线程等待和唤醒方法,需要先获取锁;一定要先await后signal。
  3. 因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞,先发放了凭证后续可以畅通无阻。

为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证,而调用两次park却需要消费两个凭证,证不够,不能放行。

16. Java内存模型之JMM

你知道什么是Java内存模型JMM吗?
JMM(Java内存模型Java Memory Model)本身是一种抽象的概念并不真实存在,它仅仅描述的是一组约定或规范。通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。

为什么要有JMM,它为什么出现?作用和功能是什么?
CPU的运行并不是直接操作内存而是先把内存里面的数据读到缓存,而内存的读和写操作的时候会造成不一致的问题。通过JMM来实现线程和主内存之间的抽象关系(线程之间的共享变量存储在主内存中,每个线程都有一个自己的本地工作内存,本地工作内存中存储了该线程用来读写共享变量的副本),屏蔽各个硬件平台和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致性的内存访问效果。

JMM没有哪些特征或者它的三大特征是什么?

  1. 可见性:是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更,JMM规定了所有的变量都存储在主内存中。
  2. 原子性:指一个操作是不可被打断的,即多线程环境下,操作不能被其他线程干扰。
  3. 有序性:为了提升性能,编译器和处理器通常会对指令序列进行重新排序。Java规范规定JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序话执行的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。指令重排可以保证串行语义一致,但没有义务保证多线程的语义也一致(可能产生“脏读”)。

JMM和volatile他们两个之间的关系?
volatile关键字:
1、可见性;2、不保证原子性;3、禁止指令重排。

happens-before先行原则你有了解过吗?

  • 在JVM中,如果一个操作执行的结果需要对另一个操作可见或者代码重排序,那么这两个操作之间必须存在happens-before(先行发生)原则,逻辑上的先后关系。包含可见性有序性的约束。

八条规则:

  1. 次序规则:一个线程内,按照代码的顺序,写在前面的操作先行发生于写在后面的操作;
  2. 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作;
  3. volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作;
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作;
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  7. 线程终止规则:线程中的所有操作都优先发生于对此线程的终止检测;
  8. 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

17. volatile与JMM

被volatile修饰的变量有什么特点?

  • 可见性和有序性,但不保证原子性;
  • 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中;
  • 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量的值;
  • volatile凭什么可以保证可见性和有序性?——> 内存屏障Memory Barrier
  • 系统底层确认变量的ACC_VOLATILE标识标志在相应的位置加入内存屏障。

内存屏障是什么?
内存屏障(也称内存栅栏,屏障指令等)是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作,避免代码重排序。

内存屏障的分类
粗分两种:

  1. 读屏障(Load Barrier):在读指令之前插入读屏障,让工作内存或CPU高速缓存 当中的缓存数据失效,重新回到主内存中获取最新数据。
  2. 写屏障(Store Barrier):在写指令之后插入写屏障,强制把缓冲区的数据刷回到主内存中。

细分四种:

屏障类型 指令示例 说明
LoadLoad Load1;LoadLoad;Load2 保证Load1的读取操作在Load2及后续读取操作之前执行
StoreStore Store1;StoreStore;Store2 在store2及其后的写操作执行前,保证Store1的写操作已经刷新到主内存
LoadStore Load1;LoadStore;Store2 在Store2及其后的写操作执行前,保证Load1的读操作已经结束
StoreLoad Store1;StoreLoad;Load2 保证Store1的写操作已经刷新到主内存后,Load2及其后的读操作才能执行

happens-before之volatile变量规则

  • 当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序,这个操作保证了volatile读之后的操作不会被重排到volatile读之前。

  • 当第一个操作为volatile写时,第二个操作为volatile读时,不能重排。

  • 当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序,这个操作保证了volatile写之前的操作不会被重排到volatile写之后。

  • 读屏障:在每个volatile读操作的后面插入一个LoadLoad屏障和LoadStore屏障。

  • 写屏障:在每个volatile写操作的前面插入StoreStore屏障;在每个volatile写操作的后面插入StoreLoad屏障。

怎么理解volatile变量的复合操作不具有原子性
对于voaltile变量具备可见性,JVM只是保证从主内存加载到线程工作内存的值是最新的,也仅仅是数据加载时是最新的。但是多线程环境下,“数据计算”和“数据赋值”操作可能多次出现,若数据在加载之后,若主内存volatile修饰变量发生修改之后,线程工作内存的操作将会作废去读主内存最新值,操作出现写丢失问题。即各线程私有内存和主内存公共内存中变量不同步,进而导致数据不一致。由此可见volatile解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改主内存共享变量的场景必须加锁同步。
举例i++的例子;

如何正确使用volatile

  1. 单一赋值可以,但是含复合运算赋值不可以;
  2. 状态标志,判断业务是否结束;
  3. 开销较低的读,写锁策略;当读远多于写,结合使用内部锁和volatile变量来减少同步的开销;
  4. DCL双端锁的发布,多线程下的解决方案适合加volatile修饰。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
    //先判断对象是否已经实例过,没有实例化过才进入加锁代码
    if (uniqueInstance == null) {
    //类对象加锁
    synchronized (Singleton.class) {
    if (uniqueInstance == null) {
    // 隐患:多线程环境下,由于重排序,该对象可能未完成初始化就被其他线程读取。
    // 1. 为 uniqueInstance 分配内存空间; 2. 初始化 uniqueInstance; 3. 将 uniqueInstance 指向分配的内存地址。
    uniqueInstance = new Singleton();
    }
    }
    }
    return uniqueInstance;
    }
    }

18. CAS

CAS是什么?有什么缺点

  • 没有CAS之前:多线程环境中使用valatile+synchronized保证线程安全i++;
  • 使用CAS之后:多线程环境中使用原子类CAS保证线程安全i++,类似于乐观锁。
    CAS(compare and swap),用于保证共享变量的原子性更新,它包含三个操作数—内存位置、预期原值与更新值。

缺点:

  1. 循环时间长开销大。底层getAndAddInt方法有一个do while,如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给CPU带来很大开销;
  2. ABA问题。可使用版本号时间戳原子引用AtomicStampedReference

CAS底层原理?谈谈对Unsafe类的理解?
Unsafe类是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,基于Unsafe类可以直接操作特定内存的数据。存在于sun.misc包中,Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的所有方法都直接调用操作系统底层资源执行相应任务。
问题:我们知道i++是线程不安全的,那AtomicInteger.getAndIncrement()如何保证原子性?

  • AtomicInteger类主要利用CAS+volatile和native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升;调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。
  • JDK提供的CAS机制,在汇编层级会禁止变量两侧的指令优化,然后使用compxchg指令比较并更新变量值(原子性)。

CAS与自旋锁
CAS是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性,以达到锁的效果。自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁。好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
实现一个自旋锁,借鉴CAS思想:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

public class SpinLockDemo {

AtomicReference<Thread> atomicReference = new AtomicReference<>();

public void lock() {
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "\t --------come in");
while (!atomicReference.compareAndSet(null, thread)) {

}
}

public void unLock() {
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName() + "\t --------task over,unLock.........");
}
}

19. 原子操作类

基本类型原子类:

  • AtomicInteger 整型原子类
  • AtomicBoolean 布尔型原子类
  • AtomicLong 长整型原子类

数组类型原子类:

  • AtomicIntegerArray 整型数组原子类
  • AtomicLongrArray 长整型数组原子类
  • AtomicReferenceArray 引用类型数组原子类

引用类型原子类:

  • AtomicReference 引用类型原子类
  • AtomicStampedReference 原子更新带有版本号的引用类型,解决修改过几次
  • AtomicMarkableReference 原子更新带有标记的引用类型,解决是否修改过,将标记戳简化为true/false

对象的属性修改原子类:

  • AtomicIntegerFieldUpdater 原子更新对象中int类型字段的值

  • AtomicLongFieldUpdater 原子更新对象中Long类型字段的值

  • AtomicReferenceFieldUpdater 原子更新对象中引用类型字段的值

    使用要求:更新的对象属性必须使用public volatile修饰符;因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。

原子操作增强类:

  • DoubleAccumulator 一个或多个变量,它们一起保持运行double使用所提供的功能更新值
  • DoubleAdder 一个或多个变量一起保持初始为零double总和
  • LongAccumulator 一个或多个变量,一起保持使用提供的功能更新运行的值long ,提供了自定义的函数操作
  • LongAdder 一个或多个变量一起维持初始为零long总和(重点),只能用来计算加法,且从0开始计算;sum()在并发情况下不保证返回精确值

热点商品点赞计算器,点赞数加加统计,不要求实时精确

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

class ClickNumber {
int number = 0;

public synchronized void clickBySynchronized() {
number++;
}

AtomicLong atomicLong = new AtomicLong(0);

public void clickByAtomicLong() {
atomicLong.getAndIncrement();
}

LongAdder longAdder = new LongAdder();

public void clickByLongAdder() {
longAdder.increment();
}

LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 0);

public void clickByLongAccumulator() {
longAccumulator.accumulate(1);
}
}

和AtomicLong比,LongAdder为什么这么快?

  • LongAdder是Striped64的子类;Striped64的基本结构里的base变量,类似于AtomicLong中全局的value;collide 扩容意向;cellsBusy 初始化cells或者cells扩容时需要获取锁,0无1有;casCellsBusy() 通过CAS操作修改cellsBusy的值,成功代表获取锁;NCPU 扩容时会用到cpu数量;getProbe() 获取当前线程的hash值;advanceProbe() 重置当前线程的hash值。
  • cell是java.util.concurrent.atomic下Striped64的一个内部类,cells数组的长度初始默认值是2,扩容为原来的2倍
  • LongAdder的基本思路就是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作(减少乐观锁的重试次数),这样热点就被分散了,冲突的概率就小很多,如果要获取真正的long值,只要将各个槽中的变量值累加返回
  • sum()会将所有的Cell数组中的value和base累加作为返回值
  • 化整为零,分散热点,空间换时间

20. ThreadLocal

ThreadLocal是什么?能干吗?
ThreadLocal提供线程局部变量,每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。不和其他线程共享,从而避免了线程安全问题。withInitial(supplier)静态方法创建线程局部变量。

ThreadLocal中ThreadLocalMap的数据结构和关系
Thread.java ——> ThreadLocal.ThredLocalMap threadLocals = null
ThreadLocal.java ——> static class ThreadLocalMap
——> static class Entry extends WeakReference<ThreadLocal<?>>

ThreadLocalMap实际上就是一个以ThreadLocal实例为Key,任意对象为value的Entry对象;当我们为ThreadLocal变量赋值,实际上就是以当前ThreadLocal实例为Key,值为value的Entry往这个ThreadLocalMap中存放。ThreadLocal本身并不存储值。

ThreadLocal的key是弱引用,这是为什么?
关于引用:

  1. 强引用:对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收,当一个对象被强引用变量引用时,它处于可达状态,是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,JVM也不会回收。除非对象引用置为null,或者超过作用域,即变为不可达,此时可以回收。
  2. 软引用:当系统内存充足时,不会被回收,当系统内存不足时,他会被回收。
  3. 弱引用:只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
  4. 虚引用:虚引用必须和引用队列联合使用,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都有可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象。虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize后,做某些事情的通知机制。虚引用被干掉,将会进入引用队列,在队列中发现有对象,则说明被GC过。

关于为什么用弱引用:

  • 当方法执行完毕后,栈帧销毁,强引用tl也就没有了,但此时线程的ThreadLocalMap里某个entry的Key引用还指向这个对象,若这个Key是强引用,就会导致Key指向的ThreadLocal对象即key指向的对象不能被gc回收,造成内存泄露;
  • 使用弱引用就可以使ThreadLocal对象在方法执行完毕后顺利被回收且entry的key引用指向为null,大概率会减少内存泄漏的问题。(还得考虑value不为null的问题

ThreadLocal内存泄漏问题你知道吗?ThreadLocal中最后为什么要加remove方法?

  • 不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄漏
  • 虽然弱引用,保证了Key指向的ThreadLocal对象能够被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄露,我们要在不使用某个ThreadLocal对象后,手动调用remove方法来删除它
  • 都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键为null的Entry对象的值(即为具体实例)以及entry对象本身从而防止内存泄漏,属于安全加固的方法。

21. AbstractQueuedSynchronizer之AQS

AQS是什么
抽象的队列同步器:

  • 是用来实现锁或者其他同步器组件的公共基础部分的抽象实现;
  • 主要用于解决锁分配给“谁”的问题;
  • 整体就是一个抽象的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态。

AQS为什么是JUC内容中最重要的基石?

  • ReentrantLock、CountDownLatch、ReentrantReadWriteLock、Semaphore等等类的内部都有抽象的静态内部类Sync继承自AQS:abstract static class Sync extends AbstractQueuedSynchronizer{};
  • 进一步理解锁和同步器的关系:锁,面向锁的使用者,定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可;同步器,面向锁的实现者,Java并发大神DoungLee,提出了统一规范并简化了锁的实现,将其抽象出来,屏蔽了同步状态管理、同步队列的管理和维护、阻塞线程排队和通知、唤醒机制等,是一切锁和同步组件实现的。

AQS的排队等候机制
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS同步队列的抽象表现。它将要请求共享资源的线程及自身的等待状态封装成队列的节点对象(Node),通过CAS、自旋以及LockSupport.park()的方式,维护着state变量的状态,使其达到同步的状态。

  • AQS内部体系架构—-内部类Node:Node的等待状态waitState成员变量;

AQS源码深度分析

  1. 公平锁和非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()—–公平锁加锁时判断等待队列中是否存在有效节点的方法。
  2. lock() ——> acquire(1) 第二个线程及后续线程抢占
  3. if(!tryAcquire(arg)&&acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
  4. addWaiter若链表没初始化就先初始化,否则入队enq(Node);compareAndSetHead和compareAndSetTail;在双向链表中,第一个节点为虚节点(也叫做哨兵节点),其实不存储任何信息,只是占位。真正的第一个有数据的节点,是从第二个节点开始的。
  5. acquireQueued中自旋tryAcquire、前置节点状态得为SIGNAL、阻塞当前节点parkAndCheckInterrupt(),里面调用的是LockSupport.park(this)。
  6. unlock() ——> release(1) ——> tryRelease(arg) 释放锁、修改资源的占有状态 ——> unparkSuccessor(h) 唤醒头结点的后置结点,里面调用的是LockSupport.unpark(s.thread)。

22. 读写锁ReentrantReadWriteLock

是什么?特点有哪些?

  • 一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程
  • 读读共存,读写互斥,写写互斥;
  • 只有在读多写少情景之下,读写锁才具有较高的性能体现。

读写锁中的锁降级

  • 将写锁降级为读锁 —— 遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级为读锁;
  • 如果一个线程持有了写锁,在没有释放写锁的情况下,它还可以继续获得读锁;
  • 从读锁升级到写锁是不可能的。

有没有比读写锁更快的锁?StampedLock邮戳锁
是什么:

  • StampedLock是JDK1.8中新增的一个读写锁,也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化;
  • stamp 代表了锁的状态。当stamp返回零时,表示线程获取锁失败,并且当释放锁或者转换锁的时候,都要传入最初获取的stamp值;
  • ReentrantReadWriteLock实现了读写分离,当前有可能会一直存在读锁,而无法获得写锁,锁饥饿;StampedLock类采取乐观获取锁,其他线程尝试获取写锁时不会被阻塞,在获取乐观读锁后,还需要对结果进行校验。

特点:

  • 所有获取锁的方法,都返回一个邮戳,stamp为零表示失败,其余都表示成功;
  • 所有释放锁的方法,都需要一个邮戳,这个stamp必须是和成功获取锁时得到的stamp一致;
  • StampedLock是不可重入的,危险(如果一个线程已经持有了写锁,在去获取写锁的话会造成死锁)
  • 三种访问模式:Reading(读模式悲观)、Writing(写模式)、Optimistic reading(乐观读模式)
  • 读的时候也可以写,如果读的时候被写了,那就重新读一次。对短的只读代码段,使用乐观模式通常可以减少争用并提高吞吐量
  • StampedLock 的悲观读锁和写锁都不支持条件变量(Condition),这个也需要注意;
  • 使用 StampedLock一定不要调用中断操作,即不要调用interrupt()方法。

23. Java对象内存布局和对象头

Object object = new Object()谈谈你对这句话的理解?

  • 位置所在——–>JVM堆->新生区->伊甸园区
  • 构成布局——–>对象头+实例数据+对齐填充
  • 对象头(在64位系统中,Mark Word占了8个字节,类型指针占了8个字节,一共是16个字节)
  • 对象标记(Mark Word)默认存储对象的HashCode、分代年龄和锁标志等信息。
  • 类元信息(类型指针):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象哪个类的实例。
  • 实例数据:存放类的属性(Field)数据信息,包括父类的属性信息。
  • 对齐填充:虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐,这部分内存按8字节补充对齐。
  • ClassLayout.parseInstance(new Object()).toPrintable()

有关压缩指针

  • Java -XX:+PrintCommandLineFlags -version 查看当前虚拟机信息
  • 默认开启压缩指针,开启后将上述类型指针压缩为4字节,以节约空间
  • 手动关闭压缩指针: -XX: -UseCompressedClassPointers

24. Synchronized与锁升级

  • Java5之前,只有Synchronized,这个是操作系统级别的重量级操作,用户态和内核态之间的频繁转换,假如锁的竞争比较激烈的话,性能下降;
  • Java6之后为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁。

锁升级功能主要依赖MarkWord中锁标志位和释放偏向锁标志位。
- 偏向锁:MarkWord存储的是偏向的线程ID;
- 轻量锁:MarkWord存储的是指向线程栈中Lock Record的指针;
- 重量锁:MarkWord存储的是指向堆中的monitor对象(系统互斥量指针)。

偏向锁
单线程竞争,当一段同步代码一直被同一个线程多次访问,由于只有一个线程那么该线程在后续访问时便会自动获得锁。当有另外一个线程逐步来竞争锁的时候,就不能再使用偏向锁了,要升级为轻量级锁,使用的是等到竞争出现才释放锁的机制。竞争线程尝试CAS更新对象头失败,会等到全局安全点(此时不会执行任何代码)撤销偏向锁,同时检查持有偏向锁的线程是否还在执行:
1. 第一个线程正在执行Synchronized方法(处于同步块),它还没有执行完,其他线程来抢夺,该偏向锁会被取消掉并出现锁升级,此时轻量级锁由原来持有偏向锁的线程持有,继续执行同步代码块,而正在竞争的线程会自动进入自旋等待获得该轻量级锁
2. 第一个线程执行完Synchronized(退出同步块),则将对象头设置为无所状态并撤销偏向锁,重新偏向。
Java15以后逐步废弃偏向锁,需要手动开启——->维护成本高。

轻量级锁
多线程竞争,但是任意时候最多只有一个线程竞争;
有线程来参与锁的竞争,但是获取锁的冲突时间极短———->本质是自旋锁CAS;
自旋一定程度和次数(Java6是默认10次;Java8 之后是自适应自旋锁——意味着自旋的次数不是固定不变的);
轻量锁和偏向锁的区别:
- 争夺轻量锁失败时,自旋尝试抢占锁;
- 轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁。

重量级锁
有大量线程参与锁的竞争,冲突性很高;
Java中synchronized的重量级锁,是基于进入和退出Monitor对象实现的。在编译时会将同步块的开始位置插入monitor enter指令,在结東位置插入monitor exit指令。
当线程执行到monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,即获取到了锁,会在Monitor的owner中存放当前线程的id,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取到这个Monitor。

锁升级后,hashcode去哪儿了?
当一个对象已经计算过一致性哈希码后,它就再也无法进入偏向锁状态了;而当一个对象当前正处于偏向锁状态,有收到需要计算其一致性哈希码请求时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁。在重量级锁的实现重,对象头指向了重量级锁的位置,代表重量级锁的ObjectMonitor类里有字段可以记录非加锁状态(标志位为01)下的Mark Word,其中存储了原来的哈希码。升级为轻量级锁时,JVM会在当前线程的栈帧中创建一个锁记录(Lock Record)空间,用于存储锁对象的Mark Word拷贝,该拷贝中可以包含identity hash code,释放锁后会将这些信息写回对象头。

锁消除
从JIT(Just In Time Compiler 即时编译器)角度看想相当于无视他,synchronized(o)不存在了;这个锁对象并没有被共用扩散到其他线程使用;极端的说就是根本没有加锁对象的底层机器码,消除了锁的使用。

1
2
3
4
5
6
7
8
9

public void m1() {
//锁消除问题,JIT会无视它,synchronized(o)每次new出来的,都不存在了,非正常的
Object o = new Object();
synchronized (o) {
System.out.println("-----------hello LockClearUpDemo" + "\t" + o.hashCode() + "\t" + object.hashCode());
}
}

锁粗化
假如方法中首尾相接,前后相邻的都是同一个锁对象,那JIT编译器会把这几个synchronized块合并为一个大块;加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提高了性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

new Thread(() -> {
synchronized (objectLock) {
System.out.println("111111111111");
}
synchronized (objectLock) {
System.out.println("222222222222");
}
synchronized (objectLock) {
System.out.println("333333333333");
}
synchronized (objectLock) {
System.out.println("444444444444");
}
//底层JIT的锁粗化优化
synchronized (objectLock) {
System.out.println("111111111111");
System.out.println("222222222222");
System.out.println("333333333333");
System.out.println("444444444444");
}
}, "t1").start();

CRS-Adversarial Examples

Evaluating the Robustness of Conversational Recommender Systems by Adversarial Examples

动机

  • 测试CRS系统的鲁棒性,考虑到人机交互时输入信息的复杂性

贡献

  • 提出了一种包含两类四种方案的对抗性评估方案,并自动生成对抗性实例来评估这些系统在面对不同输入数据时的鲁棒性。
  • 对三个CRS模型进行了对抗测试,结果都是较差的鲁棒性

四种方案

  1. expecting the same prediction by changing the user’s answer
    期望得到相同的预测,但改变用户的回答

  1. expecting the same prediction by adding more details to the user’s answer
    期望得到相同的预测,但为用户的回答中添加更多的信息

  1. expecting a different prediction by
    changing the user’s answer
    期望得到不同的预测,但改变用户的回答

  1. expecting a different prediction by adding a contradictory sentence to the user’s answer
    期望得到不同的预测,但为用户的回答中添加一句矛盾的句子

结果分析及启发

  • We believe that if a model needs to be successful in this task, should have a strong attention mechanism on the user’s answer. This helps the system to fully understand user preferences.
  • first needs to decide which words are important in the user’s answer, then consider all these important words (which are user preferences) in the recommendation.

这篇文章通过一些对抗实例来测试如今一些好的CRS模型,发现其鲁棒性较差的问题。大部分性能都是下降了,说明在复杂句子中去理解单词的重要性以充分理解用户偏好对CRS性能提升很重要。但也有略微上升的情况,是否可以说明增加适当的对抗或者说干扰,对模型的鲁棒性有一定的提升?
带来的思考在于,对抗与非对抗之间的平衡,一方面CRS模型要能抵御一些句子层面上的干扰,另一方面能将干扰化为己用来提高性能。

Hello World

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

some ideas

Some ideas

对话推荐

  1. Conversation Module:对话模块,负责理解用户的自然语言反馈以及给出系统的文本答复
  2. Conversation Module:策略模块,根据当前状态做出如何回复的决定,是继续询问还是进行推荐
  3. Recommender Module:推荐模块,负责根据用户的偏好给出相应的推荐列表或者单个推荐结果

基于属性的对话推荐

更多地针对策略模块,希望在最短的交谈次数内实现尽可能精确的推荐。

单轮场景

SIGIR 2018(CRM):Conversational Recommender System

  • 动机:如何准确地理解用户的意图?如何实施序列化的决策并在每一步采取合适的行动?如何做个性化的推荐以最大化地提升用户的满意程度?
  • 方法:提出三个模块分别解决上述三个问题。Belief Tracker + Policy Network + Recommender
    1. 对于一句话,利用n-gram词表转化为向量,LSTM 网络学习所有对话的隐含表示
    2. 推荐模块采用2-way FM二路因子分解机,输入为用户信息、物品信息和对话信息,输出为用户对物品的预测评分
    3. 策略模块,输入对话信息,简单的两层前馈神经网络,输出用户的动作预测 。利用强化学习中的 Policy Gradient 方法进行训练

多轮场景

WSDM 2020(EAR):Estimation–Action–Reflection: Towards Deep Interaction Between Conversational and Recommender Systems

  • 动机:认为对话推荐应该采用多轮场景,系统能对一个用户进行多次推荐,并根据之前推荐的反馈改善后面的推荐
  • 方法:加强对话模块和推荐模块的交互(What attributes to ask? When to recommend items? How to adapt to users’ online feedback?)
    Estimation–Action–Reflection框架:
    1. Estimation 模块完成推荐任务,FM + BPR
    2. Action 模块负责决策,基于强化学习训练,网络结构同CRM,更简单的奖励机制
    3. Reflection 模块以负反馈物品为负例构建新的损失函数项进一步训练

搜索剪枝

KDD 2022(CPR):Interactive Path Reasoning on Graph for Conversational Recommendation

  • 动机:之前的工作都是用隐式的方法来利用用户在属性方面的反馈信息
  • 方法:本文基于图机构(由用户、物品、属性三类节点构成),通过显式方法来利用用户喜欢的属性,删去了大量无关的候选物品。

  1. 将决策的动作空间缩小为两个,询问or推荐
  2. 持久化地维护之前访问过的属性节点集合、询问用户时被予以否定的属性集合和候选物品集合。显式地利用用户的反馈信息确定了物品和属性的候选范围
  3. 在候选集合中进一步排序,与EAR类似

统一架构

SIGIR 2021(UNICORN):Unified Conversational Recommendation Policy Learning via Graph-based Reinforcement Learning

  • 动机:过去基于属性的对话推荐系统工作总是将决策过程分到多个模块去完成,这对模型的可扩展性造成了影响
  • 方法:将三个决策问题(询问还是推荐、询问什么、推荐什么)统一起来

  1. Graph-based MDP Environment:基本上沿用了 CPR 构建的图上路径推理机制
  2. Graph-enhanced State Representation Learning:将当前对话状态表示成一个向量
  3. Action Selection Strategy:强化学习动作空间,两类动作(推荐和询问)各选 k1 和 k2 个,选择top-k1个物品推荐,选择top-k2个属性进行询问
  4. Deep Q-Learning Network:强化学习决策网络模块,输入是2中的对话状态向量和3中的k1 + k2个节点的embedding,采用DQN算法进行学习训练,输出是??

生成式对话推荐

更注重向用户提供流畅的对话体验,同时将推荐物品相关的信息融入到回复文本中,提高推荐的可解释性。

降噪协同

NeurIPS 2018(ReDial):Towards Deep Conversational Recommendations

  • 构造了电影场景下的对话推荐数据集REDIAL(包含 6924 部电影、956 位用户、11348 个对话,平均每个对话由 18 个英文句子构成,并保证每个对话中至少提到 4 部电影,还包含语句的情感判断标签)
  • 先按模块分别用更大的数据集训练,然后再一起端到端地用 REDIAL 训练
  • 对话模块中,使用层次化的RNN结构HRED来编码对话,并采用了预训练的 Gensen 句子向量表示。HRED 得到对话的隐含向量表示之后,一方面会给情感分析 RNN 进行计算,一方面给 Switching Decoder 来将推荐结果融入到对话中,另外还能用于给 RNN 解码出回复语句
  • 推荐模块中,采用降噪自编码器,同时利用了情感分析模块的计算结果进一步修正推荐结果
  • 后续的工作基本上都不再利用情感标签

知识增强

EMNLP 2019(KBRD):Towards Knowledge-Based Recommender Dialog System

  • 动机:ReDial当对话中没有提到物品时则无法向用户提供推荐结果
  • 方法:引入知识图谱,从中抽取与 REDIAL 任务相关的子图,将上面的节点与 REDIAL 中的实体(包括电影实体和如导演、演员、影片类别等其他实体)进行对齐;R-GCN 学习图上实体的向量表示;采用自注意力机制来进行融合不同实体对应的 embedding,从而得到用户表示;最终通过向量内积来计算每个实体的得分;对话模块采用 Transformer 架构,并在最终生成的词汇分布上加上 Vocabulary Bias
  • 对话模块获得的实体信息对推荐产生促进作用
  • 推荐模块产生的用户embedding转化为Vocabulary Bias来促进对话生成

语义融合

KDD 2020(KGSF):Improving Conversational Recommender Systems via Knowledge Graph based Semantic Fusion

  • 动机:过去的生成式对话推荐系统工作没能很好地处理文本信息,基于自然语言的表达与基于实体构建的用户偏好表示二者存在天然的语义差异

  • 方法:通过互信息最大化的多知识图谱语义融合技术,打通不同类型信息之间的语义差异

    1. 实体信息方面,DBpedia 子图进行编码
    2. 词汇信息方面,与 ConceptNet 中的词汇对齐
    3. 使用互信息最大化算法来对两类 embedding 进行预训练
    4. 对话模块,KGSF 修改了 Transformer 的内部结构,将用户或者说对话构造的实体矩阵、词汇矩阵通过多头自注意力机制逐步融入解码过程中

话题引导

COLING 2020(TGReDial):Towards Topic-Guided Conversational Recommender System

  • 动机:缺乏主动的引导来将非推荐场景的对话转变为推荐场景的对话;由数据标注平台的工作人员对话来生成的对话推荐数据集难以捕捉现实世界场景丰富且复杂的情况

  • 方法:
    1. 收集电影序列,对于某一用户可以从其观影记录中的电影构建若干子序列,其中每个序列的电影都有相同的话题标签
    2. 构建话题转移的通道,作者借助深度优先搜索算法,在知识图谱 ConceptNet 上找到一条从最初话题(greeting)到目标电影话题的转移路径
    3. 利用深度学习模型从话题生成对话文本,并进行人工修改和润色,以保证对话的流畅性

可控生成

EMNLP 2021(NTRD):Learning Neural Templates for Recommender Dialogue System

  • 动机:不能总是将推荐结果精确且恰当地融入到生成的回复中;推荐结果总是在训练集中提到的物品,缺乏泛化性

  • 方法:

    1. 将 KGSF 的对话模块改造为 Response Template Generator,具体做法是将数据集中的所有物品(电影)全部替换为特殊符号[ITEM],使得生成的对话不带具体的电影信息,而是一个个句子模板
    2. 构造一个物品选择器来填写模板中的[ITEM]槽,NTRD 使用堆叠的多头注意力函数来构造选择器。在这个堆叠的结构中逐步融入了模板词汇相关信息、模板的槽相关信息、推荐模块得到的候选物品相关信息

UCCR

User-Centric Conversational Recommendation with Multi-Aspect User Modeling

SIGIR 2022 以用户为中心的对话推荐系统

  • 动机:现有方法本质上当前会话的建模,而忽略了用户建模。而本文发现用户历史会话相似用户信息也可以很好地辅助用户兴趣建模,特别是在用户当前会话信息较少(冷启动)的场景下效果更佳。
  • 方法:在历史会话建模部分,UCCR同时考虑了用户的实体偏好、语义偏好和消费偏好,从这三种偏好中提取有益于用户当前兴趣建模的信息;之后UCCR基于对比学习,学习不同用户当前/历史兴趣偏好之间的内在联系;在查找相似用户部分,UCCR考虑了用户兴趣的动态变化过程,基于用户历史兴趣查找相似用户;最终将多维度的用户信息融合在一起。
  1. 历史对话学习器:从历史对话中提取用户multi-view兴趣偏好,包含实体偏好(用户提到的实体)、语义偏好(用户提到的单词)、消费偏好(用户历史喜欢的商品)。
  2. 多视图兴趣偏好映射器:学习不同view的兴趣偏好间的内在联系。其核心思想是:同一用户的不同view的兴趣偏好应当相关,而不同用户之间应当无关。
  3. 时序相似用户选择器:历史兴趣相似的用户,其当前兴趣有更大概率会相似。
  4. 用户兴趣偏好融合:平衡当前对话信息与multi-aspect信息之间的关系

  • Current Session Learner:对当前对话建模,与KGSF和KBRD差不多,DBpedia和ConceptNet分别编码实体和单词
  • Historical Sessions Learner:
    1. Historical Entity Learner:用户历史提到的所有实体,与当前实体的相似度进行加权平均,目的是更多地选择和当前实体偏好相似的历史实体,以防止不相关信息干扰
    2. Historical Word Learner:建模近因效应提取到适当的语义知识,利用每个单词出现的对话轮数对齐加权平均
    3. Historical Item Learner:历史item,利用当前实体表示和当前单词表示的组合来代替当前商品表示,最终得到历史商品表示
  • Multi-View Preference Mapper:基于对比学习,学习不同view的内在信息,进而得到更准确地表示
  • Temporal Look-Alike User Selector:由于CRS中用户兴趣随着对话推进不断变化,将用户每一次交互历史都和target用户历史进行比较,学习其中最有用的信息
  • Multi-Aspect User-Centric Modeling:Entity-View + Word-View + Item-View

ReDial英文数据集 和 TG-ReDial中文数据集
利用对话时间对数据集进行重排

UniCRS

KDD 2022:Towards Unified Conversational Recommender Systems via Knowledge-Enhanced Prompt Learning

Challenges

  • 三个子模块的共同优化
  • 推荐系统的去偏
  • 精心设计的多轮对话策略
  • 引入更多的知识如多模态数据
  • 更合理的评测机制和更好的用户模拟器

pytorch study

PyTorch深度学习实践学习记录

0 配置环境

  • conda环境:’pytorch’

      conda create -n pytorch python=3.9  
    
  • GPU:NVIDIA GeForce GTX 1050 Ti

  • 安装cuda

     conda install pytorch==1.10.1 torchvision==0.11.2 torchaudio==0.10.1 cudatoolkit=10.2
    
  • 测试一下

      import torch  
      torch.cuda.is_available()  //成功则返回True
    
  • 安装pycharm和jupyter notebook

  • dir()和help()函数

1 加载数据

  1. Dataset 提供一种方式去获取数据及其label
  • 如何获取每一个数据及其label?

      def __init__(self, root_dir, label_dir)  
      def __getitem__(self, idx):
    

三种数据集结构 1.label是文件夹名 2.label在txt文件中 3.label包含在文件名中

  • 告诉我们总共有多少的数据?

      def __len__(self):
          return len(self.img_path_list)
    
  1. Dataloader 为后面的网络提供不同的数据形式

     test_loader = DataLoader(dataset=test_data, batch_size=64,shuffle=True, num_workers=0)
    
     writer = SummaryWriter("dataloader_logs")
     for epoch in range(2):
             step = 0
             for data in test_loader:
                     imgs, targets = data
                     writer.add_images("epoch: {}".format(epoch), imgs, step)
                     step = step + 1 
    

2 tensorboard的使用

启动tensorboard

    tensorboard --logdir=logs --port=6007

example:

    writer = SummaryWriter("logs")

    image_path = "dataset/train/ants/0013035.jpg"
    img_PIL = Image.open(image_path)
    img_array = np.array(img_PIL)
    writer.add_image("train", img_array, 1, dataformats='HWC')
    # y = 2x
    for i in range(100):
    writer.add_scalar("y=2x", 2*i, i)
    writer.close()  

3 transform的使用

关注输入和输出类型,多看官方文档,关注方法需要的参数

  1. ToTensor()

     tensor_trans = transforms.ToTensor()
     tensor_img = tensor_trans(img)
    
  2. Normalize

     trans_norm = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
     img_norm = trans_norm(tensor_img)  
    
  3. Resize

     trans_resize = transforms.Resize((256, 256))  
    
  4. Compose

     trans_resize_2 = transforms.Resize(256)
     trans_compose = transforms.Compose([trans_resize_2, tensor_trans])
     img_resize_2 = trans_compose(img)  
    
  5. RandomCrop

     trans_random = transforms.RandomCrop(256)
     trans_compose_2 = transforms.Compose([trans_random, tensor_trans])
    

4 dataset 和 transform 一起使用

    dataset_transform = torchvision.transforms.Compose([
            torchvision.transforms.ToTensor() 
            ])
    train_set = torchvision.datasets.CIFAR10(root="./dataset_CIFAR10",transform=dataset_transform, train=True, download=True)
    test_set = torchvision.datasets.CIFAR10(root="./dataset_CIFAR10", transform=dataset_transform, train=False, download=True)  

5 Neural Network

1 nn.Module

Base class for all neural network modules
example:

    import torch.nn as nn
    import torch.nn.functional as F

    class Model(nn.Module):
    def __init__(self):
            super().__init__()
            self.conv1 = nn.Conv2d(1, 20, 5)
            self.conv2 = nn.Conv2d(20, 20, 5)

    def forward(self, x):
            x = F.relu(self.conv1(x))
            return F.relu(self.conv2(x))  

2 nn.Conv2d

Parameters:

  • in_channels (int) – Number of channels in the input image

  • out_channels (int) – Number of channels produced by the convolution

  • kernel_size (int or tuple) – Size of the convolving kernel

  • stride (int or tuple, optional) – Stride of the convolution. Default: 1

  • padding (int, tuple or str, optional) – Padding added to all four sides of the input. Default: 0

  • padding_mode (str, optional) – ‘zeros’, ‘reflect’, ‘replicate’ or ‘circular’. Default: ‘zeros’

  • dilation (int or tuple, optional) – Spacing between kernel elements. Default: 1 空洞卷积

  • groups (int, optional) – Number of blocked connections from input channels to output channels. Default: 1

  • bias (bool, optional) – If True, adds a learnable bias to the output. Default: True

3 nn.MaxPool2d

又叫下采样, nn.MaxUnpool2d 为上采样
Parameters:

  • kernel_size (Union[int, Tuple[int, int]]) – the size of the window to take a max over

  • stride (Union[int, Tuple[int, int]]) – the stride of the window. Default value is kernel_size

  • padding (Union[int, Tuple[int, int]]) – Implicit negative infinity padding to be added on both sides

  • dilation (Union[int, Tuple[int, int]]) – a parameter that controls the stride of elements in the window

  • return_indices (bool) – if True, will return the max indices along with the outputs. Useful for torch.nn.MaxUnpool2d later

  • ceil_mode (bool) – when True, will use ceil instead of floor to compute the output shape

4 Non-linear Activations

  • nn.ReLU

  • nn.Sigmoid
    效果:

5 nn.Sequential

example:

    self.model1 = Sequential(
        Conv2d(3, 32, 5, padding=2),
        MaxPool2d(2),
        Conv2d(32, 32, 5, padding=2),
        MaxPool2d(2),
        Conv2d(32, 64, 5, padding=2),
        MaxPool2d(2),
        Flatten(),
        Linear(1024, 64),
        Linear(64, 10)
    )  

使用已有模型开发

    vgg16_false = torchvision.models.vgg16(pretrained=False)
    vgg16_true = torchvision.models.vgg16(pretrained=True)

vgg16预训练是在ImagNet上,但ImagNetl类别是1000个,Cifar10类别是10个
方案1:add_module添加一个线性层

    vgg16_true.classifier.add_module('add_linear', nn.Linear(1000, 10))

方案2:直接修改某一层

    vgg16_false.classifier[6] = nn.Linear(4096, 10)

模型的保存与导入(两种)

  1. torch.save(vgg16_false, “vgg16_method1.pth”)
    model = torch.load(“vgg16_method1.pth”)

  2. (recommend) smaller 通过字典形式存储参数

    torch.save(vgg16_false.state_dict(), "vgg16_method2.pth")   
    vgg16_false.load_state_dict(torch.load("vgg16_method2.pth"))
    

完整的训练流程(套路)

  1. 准备数据集

  2. dataloader加载数据集

  3. 搭建网络模型

  4. 创建网络模型实例

  5. 定义损失函数

  6. 定义优化器

  7. 设置网络训练的参数

  8. 开始训练

  9. 验证模型

  10. 最后保存模型

  11. tensorboard训练结果展示

    # 准备训练数据集
    train_data = torchvision.datasets.CIFAR10(root="./dataset_CIFAR10", train=True, transform=torchvision.transforms.ToTensor(),download=True)
    
    # 准备训练数据集
    test_data = torchvision.datasets.CIFAR10(root="./dataset_CIFAR10", train=False, transform=torchvision.transforms.ToTensor(),download=True)
    print("训练数据集的长度为:{}".format(len(train_data)))
    
    # 利用DataLoader来加载数据集
    train_dataloader = DataLoader(train_data, batch_size=64)
    test_dataloader = DataLoader(train_data, batch_size=64)
    
    # 创建网络模型
    model = Model()
    
    # 损失函数
    loss_fn = nn.CrossEntropyLoss()
    
    # 优化器
    learning_rate = 1e-2
    optimizer =  torch.optim.SGD(model.parameters(), lr=learning_rate)
    
    # 设置训练网络的一些参数
    # 记录训练的次数
    total_train_step = 0
    # 记录测试的次数
    total_test_step = 0
    # 训练的轮数
    epoch = 10
    
    # 添加tensorboard
    writer = SummaryWriter("train_logs")
    
    for i in range(epoch):
    print("--------第 {} 轮训练开始--------".format(i+1))
    # 训练步骤
    # model.train()  # 适用于有dropout层,Norm层
    for data in train_dataloader:
            imgs, targets = data
            outputs = model(imgs)
            loss = loss_fn(outputs, targets)
            # 优化器优化模型
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            total_train_step = total_train_step + 1
            if total_train_step % 100 == 0:
            print("训练次数:{},loss:{}".format(total_train_step, loss.item()))
            writer.add_scalar("train_loss", loss.item(), total_train_step)
    
    # 测试步骤
    # model.eval()  # 适用于有dropout层,Norm层
    total_test_loss = 0.0
    with torch.no_grad():
            for data in test_dataloader:
            imgs, targets = data
            outputs = model(imgs)
            loss = loss_fn(outputs, targets)
            total_test_loss += loss.item()
            accuracy = (outputs.argmax(1) == targets).sum()
            total_accuracy = total_accuracy + accuracy
    print("整体测试集上的Loss:{}".format(total_test_loss))
    print("整体测试集上的accuracy:{}".format(total_accuracy/len(test_data)))
    writer.add_scalar("test_loss", total_test_loss, total_test_step)
    writer.add_scalar("test_accuracy", total_accuracy/len(test_data), total_test_step)
    total_train_step += 1
    
    # 保存模型
    # torch.save(model, "model_{}.pth".format(i))
    # print("模型已保存")
    writer.close()
    

使用GPU训练

对模型、损失函数、要训练和验证数据处理,两种方式:

  1. .cuda()
  2. 先定义device = torch.device(“cuda”),然后对模型、损失函数、数据使用.to(device)
    可使用google.colab使用免费的计算资源

验证的流程

    image_path = ""
    image = Image.open(image_path)

    transform = torchvision.transforms.Compose([torchvision.transforms.Resize((32, 32)),
                                            torchvision.transforms.ToTensor()])

    image = transform(image)

    # 载入网络模型
    # 如果是gpu上训练的结果在cpu上载入,要加map_location=torch.device('cpu')
    model = torch.load("model_29_gpu.pth", map_location=torch.device('cpu'))

    image = torch.reshape(image, (1, 3, 32, 32))
    model.eval()
    with torch.no_grad():
    output = model(image)
    print(output)
    print(output.argmax(1))

Knowledge-Enhanced Prompt Learning

Towards Unified Conversational Recommender Systems via Knowledge-Enhanced Prompt Learning

通过知识增强的提示学习实现统一的CRS KDD 2022

摘要

  • propose a unified CRS model named UniCRS based on knowledge-enhanced prompt learning.
  • unifies the recommendation and conversation subtasks into the prompt learning paradigm, and utilizes knowledge-enhanced prompts based on a fixed pre-trained language model (PLM) to fulfill both subtasks in a unified approach.
  • In the prompt design, we include fused knowledge representations, task-specific soft tokens,and the dialogue context, which can provide sufficient contextual information to adapt the PLM for the CRS task.
  • for the recommendation subtask, we also incorporate the generated response template as an important part of the prompt, to enhance the information interaction between the two subtasks.

动机

  • Existing works either design semantic alignment strategies, or share knowledge resources and representations between the two modules. However, these approaches still rely on different architectures or techniques to develop the two modules, making it difficult for effective module integration.
    现有的工作要么设计语义对齐策略,要么在两个模块之间共享知识资源和表示。然而,这些方法仍然依赖于不同的架构或技术来开发这两个模块,这使得有效的模块集成变得困难。
  • it has been pointed out that the generated responses from the conversation module do not always match the predicted items from the recommendation module

方法

用于提示学习的语义融合

  • 根据先前的研究,本文将 KG 合并为特定于任务的知识资源,因为它涉及对话中提到的实体和项目的有用知识。然而,已经发现对话的语义空间和 KGs 之间存在很大的语义差距。 本文需要首先融合两个语义空间以进行有效的知识对齐和丰富。 特别是,这一步的目的是融合来自不同编码器的令牌和实体嵌入。
  1. 编码词标记和知识图谱实体:给定对话历史 ,本文首先将对话历史中出现的对话词和 KG 实体分别编码为词嵌入和实体嵌入。 为了补充本文的基础 PLM DialoGPT(单向解码器),本文采用了另一个固定的 PLM RoBERTa(双向编码器)来推导词嵌入。
  2. 词实体语义融合:为了弥合词和实体之间的语义鸿沟,本文使用交叉交互机制通过双线性变换将两种语义表示联系起来:
  3. 预训练融合模块:为了更好地优化融合模块的参数,提出了一种基于提示的预训练方法,它利用了对话中的自我监督信号

特定于子任务的提示设计

  • 虽然基础 PLM 是固定的,无需微调,但本文可以设计特定的提示以使其适应 CRS 的不同子任务。 对于每个子任务(推荐或对话),提示的主要设计由三部分组成,即对话历史、子任务特定软令牌和融合知识表示。 对于推荐,本文进一步将生成的响应模板合并为额外的提示标记。 接下来,本文详细描述这两个子任务的具体提示设计。
  1. 提示生成响应:响应生成的提示由原始对话历史记录(以单词标记 的形式)、特定于生成的软标记(以潜在向量 P 的形式)和融合的文本上下文(以潜在向量 T~ 的形式)组成。
  2. 提示项目推荐:项目推荐提示由原始对话历史 (以单词标记的形式)、特定于推荐的软标记 P(以潜在向量的形式)、融合实体上下文 E~(以潜在向量的形式)和 响应模板 (以单词标记的形式)。

实验和结果


KGSF > KBRD > ReDial。 KGSF 和 KBRD 都将外部 KG 纳入其推荐模块中,这可以丰富对话历史中提到的实体的语义,以更好地捕捉用户意图和偏好。 此外,KGSF 还采用互信息最大化的方法来进一步改进实体表示。 对于四个预训练模型,本文可以看到 BERT 和 BART 的表现优于 GPT-2 和 DialoGPT。 原因可能是 GPT-2 和 DialoGPT 基于单向 Transformer 架构,这限制了它们的对话理解能力。 此外,可以看到 BART 在 ReDial 数据集上实现了相当的性能,甚至优于 BERT。 这表明 BART 也可以很好地理解推荐任务的对话语义。最后,可以看到本文模型大大优于所有基线。 本文利用专门设计的提示来指导基础 PLM,并结合 KG 通过预训练任务提高提示质量。

启发

flask+vue+mysql前后分离项目每日记录

flask+vue+mysql前后分离项目每日记录

Day1

创建项目

  • conda创建虚拟环境:flask-vue 安装第三方库:requirement.txt
  • 前端用的vue-admin-template-master,在此基础上改造开发
  • 新建mysql数据库movies

flask配置数据库

创建config.py 文件,配置数据库

SQLALCHEMY_DATABASE_URI = "mysql://root:123456@localhost:3306/movies"
SQLALCHEMY_COMMIT_ON_TEARDOWN = True
SQLALCHEMY_TRACK_MODIFICATIONS = False    

设计user表和movie表(以user表为例)

class Users(db.Model):
    __tablename__ = 'user'
    userid = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(
        db.String(255),
        nullable=False,
    )
    ...
    created_on = db.Column(
        db.DateTime,
        nullable=False,
    )
    enable = db.Column(
        db.Boolean,
        default=True,
        nullable=False,
    )

    def set_password(self, password: str):
        self.password = generate_password_hash(password, method='sha512')

    def check_password(self, password: str):
        return check_password_hash(self.password, password)

    def __repr__(self):
        return '<User {}>'.format(self.name)

    @classmethod
    def add(cls, user):
        db.session.add(user)
        db.session.commit()

命令行一次性创建所有表

python run.py create_db   //注意切换到项目的conda环境

movie数据

  • MovieLens Latest Datasets
  • ml-latest-small.zip
  • Small: 100,000 ratings and 3,600 tag applications applied to 9,000 movies by 600 users. Last updated 9/2018.
    通过爬虫从IMBD网站中获取电影的详细信息和封面图片。参考链接 这里直接使用其爬好的最终csv文件,info.csv
    mysql中导入csv,注意字段的长度!longtext > text > varchar(255)

Day2

用户登录

    def post(self):
        parser = reqparse.RequestParser()
        parser.add_argument('username',
                            required=True,
                            nullable=False,
                            type=str,
                            location='json',
                            help="username is required")
        parser.add_argument('password',
                            required=True,
                            nullable=False,
                            type=str,
                            location='json',
                            help="password is required")
        args = parser.parse_args()
        username = args.get('username')
        password = args.get('password')
        user = Users.query.filter_by(name=username).first()
        if user is None:
            error = 'User not exists'
        elif user.check_password(password=password):
            access_token = create_access_token(
                identity=user.userid, expires_delta=datetime.timedelta(days=1))
            return jsonify({'access_token': access_token})
        else:
            error = 'user and password not match'
        return error, 401  

username与password匹配则返回jwt_token;前端发送请求需携带jwt_token,例如获取用户信息显示在页面头部位置。

    @jwt_required()
    def get(self):
        userid = get_jwt_identity()
        user = Users.query.filter_by(userid=userid).first()
        return jsonify({
            'username':
                user.name,
            'email':
                user.email
            })  

postman测试


如果post请求参数是json需在boby里写。

遇到的问题

我把它定义为Flask和Vue在使用JWT过程中的跨域问题。因为使用了flask_cros中间件,其他的方法都是不存在跨域问题的,单单在前端携带token发送请求时报了错。如图:

后端控制台显示丢失head信息。

但postman测试,如果’Authorization’字段为’Bearer ‘ + token,则可以请求成功。于是把前端的request拦截器中携带token发送请求时也加上’Bearer ‘的头部信息,则成功解决。
查阅资料:在token前面加上Bearer是一种规范, W3C 的 HTTP 1.0 规范,Authorization 的格式是:
Authorization: <type> <authorization-parameters>
Bearer 常见于 OAuth 和 JWT 授权。

明天目标

  1. 用户注册
  2. 用户首页设计
  3. 系统整体功能模块设计

About ChatGPT

ChatGPT背后的工作原理

原文链接

介绍

  • ChatGPT是OpenAI发布的最新语言模型,能以不同样式、不同目的生成文本,并且在准确度、叙述细节和上下文连贯性上具有更优的表现。
  • OpenAI使用监督学习和强化学习的组合来调优ChatGPT,其中的强化学习组件使ChatGPT独一无二。OpenAI使用了「人类反馈强化学习」(RLHF)的训练方法,该方法在训练中使用人类反馈,以最小化无益、失真或偏见的输出。

GPT-3

非一致模型,基于来自互联网的大量文本数据进行训练,能够生成类似人类的文本,但它们可能并不总是产生符合人类期望的输出。它们的目标函数是词序列上的概率分布,用来预测序列中的下一个单词是什么。一致性问题表现为:

  • 提供无效帮助:没有遵循用户的明确指示。
  • 内容胡编乱造:虚构不存在或错误事实的模型。
  • 缺乏可解释性:人们很难理解模型是如何得出特定决策或预测的。
  • 内容偏见有害:一个基于有偏见、有害数据训练的语言模型可能会在其输出中出现这种情况,即使它没有明确指示这样做。

语言模型训练策略

  1. Next-token-prediction
  2. masked-language-modeling
    一般来说,这些训练策略可能会导致语言模型在一些更复杂的任务中出现不一致,因为一个仅被训练来预测文本序列中的下一个词的模型可能不一定会学习其含义的某些更高级表征。因此,该模型很难推广到需要对语言更深入理解的任务。

RLHF

ChatGPT 基于最初的 GPT-3 模型,但为了解决模型的不一致问题,使用了人类反馈来指导学习过程,对其进行了进一步训练。方法总体上包括三个不同步骤:

  1. 有监督的调优:预训练的语言模型在少量已标注的数据上进行调优,以学习从给定的 prompt 列表生成输出的有监督的策略(即 SFT 模型);
  2. 模拟人类偏好:标注者们对相对大量的 SFT 模型输出进行投票,这就创建了一个由比较数据组成的新数据集。在此数据集上训练新模型,被称为训练回报模型(Reward Model,RM);
  3. 近端策略优化(PPO):RM 模型用于进一步调优和改进 SFT 模型,PPO 输出结果是策略模式。
    步骤 1 只进行一次,而步骤 2 和步骤 3 可以持续重复进行:在当前最佳策略模型上收集更多的比较数据,用于训练新的 RM 模型,然后训练新的策略。

监督调优模型

  • 数据收集:选择一个提示列表,标注人员按要求写下预期的输出。对于 ChatGPT,使用了两种不同的 prompt 来源:一些是直接使用标注人员或研究人员准备的,另一些是从 OpenAI 的 API 请求(即从 GPT-3 用户那里)获取的。虽然整个过程缓慢且昂贵,但最终得到的结果是一个相对较小、高质量的数据集(大概有 12-15k 个数据点),可用于调优预训练的语言模型。
  • 模型选择:GPT-3.5 系列中的预训练模型,text-davinci-003
    这里的问题是监督学习步骤具有高可扩展性成本。
    为了克服这个问题,使用的策略是让人工标注者对 SFT 模型的不同输出进行排序以创建 RM 模型,而不是让人工标注者创建一个更大的精选数据集。

训练回报模型

这一步的目标是直接从数据中学习目标函数。该函数的目的是为 SFT 模型输出进行打分,这代表这些输出对于人类来说可取程度有多大。这强有力地反映了选定的人类标注者的具体偏好以及他们同意遵循的共同准则。最后,这个过程将从数据中得到模仿人类偏好的系统。

  • 选择 prompt 列表,SFT 模型为每个 prompt 生成多个输出(4 到 9 之间的任意值);
  • 标注者将输出从最佳到最差排序。结果是一个新的标签数据集,该数据集的大小大约是用于 SFT 模型的精确数据集的 10 倍;
  • 此新数据用于训练 RM 模型 。该模型将 SFT 模型输出作为输入,并按优先顺序对它们进行排序。
    对于标注者来说,对输出进行排序比从头开始打标要容易得多,这一过程可以更有效地扩展。

PPO模型微调SFT模型

强化学习被应用于通过优化 RM 模型来调优 SFT 模型。近端策略优化(PPO),近端策略优化模型。 PPO 可以根据所采取行动的估计价值对策略进行更明智的更新。PPO 模型由 SFT 模型初始化,价值函数由 RM 模型初始化。该环境是一个「bandit environment」,它会产生随机 prompt 并期望对 prompt 做出响应。对于给定的 prompt 和响应,它会产生相应的回报(由 RM 模型决定)。SFT 模型会对每个 token 添加 KL 惩罚因子,以尽量避免 RM 模型的过度优化。

局限性

  1. 会受到各种错综复杂的主观因素的影响
  2. 缺乏对照研究

相关论文