JavaSE 1.0 版本

JavaSE

JavaSE

大家好,我是小笙,经过几个月的总结学习,我完成了javaSE1.0版本的笔记总结,我分享出来,希望对大家了解java基础,我在这里推荐java视频:韩顺平老师的java基础视频以及Java 全栈知识体系文档,受益匪浅!我们一起加油!


Java概述

Java诞生

Java(英式发音[ˈʤɑːvə],美式发音[ˈʤɑvə])是一种广泛使用的计算机编程语言.

  1. 嵌入式系统的发展,c/c++在不同平台上的开发需要不同的编译器,然而创建编译器"费时费力"

  2. 开发者研究开发一种可移植,跨平台的计算机语言

  3. 1991 Oak的出现 ~~> 1995 更名为Java

开发Java语言的初衷

Java之所以被开发,是要达到以下五个目的:

​ 初衷 <==> 语言特点

  • 应当使用面向对象程序设计方法学 <==> 面向对象
  • 应当允许同一程序在不同的计算机平台执行 <==> 平台无关性
  • 应当包括内建的对计算机网络的支持 <==> 分布式
  • 应当被设计成安全地执行远端代码 <==> 安全性
  • 应当易于使用,并借鉴以前那些面向对象语言(如C++)的长处。 <==> 简单性,健壮性,高性能

java语言有哪些特点?

面向对象的三大基本特征和五大基本原则

Java语言特点

1. 面向对象

Java 是一种面向对象的语言,它对对象中的类、对象、继承、封装、多态、接口、包等均有很好的支持。使用 Java 语言开发程序,需要采用面向对象的思想设计程序和编写代码。

2. 平台无关性

平台无关性的具体表现在Java 是“一次编写,到处运行“的语言,因此采用 Java 语言编写的程序具有很好的可移植性,而保证这一点的正是 Java 的虚拟机机制。在引入虚拟机之后,Java 语言在不同的平台上运行不需要重新编译。

image-20211211145530665

3. 简单性

Java 语言的语法与 C 语言和 C++ 语言很相近,使得很多程序员学起来很容易。对 Java 来说,它舍弃了很多 C++ 中难以理解的特性,如操作符的重载和多继承等,而且 Java 语言不使用指针,加入了垃圾回收机制,解决了程序员需要管理内存的问题,使编程变得更加简单。

4. 编译与解释并存

Java 程序在 Java 平台运行时会被编译成字节码文件,然后可以在有 Java 环境的操作系统上运行。在运行文件时,Java 的解释器对这些字节码进行解释执行,执行过程中需要加入的类在连接阶段被载入到运行环境中。

Java即是编译型的,也是解释型语言,总的来说Java更接近解释型语言(补充:解释型语言:javascript,PHP,Java 编译型语言:c/c++)

解释型语言和编译型语言的区别:

解释性语言,编译后的代码,不能直接被机器执行,需要解释器来执行 ; 编译性语言,编译后的代码,可以直接被机器执行,c/c++

5. 多线程

Java 语言是多线程的,这也是 Java 语言的一大特性,Java 支持多个线程同时执行,并提供多线程之间的同步机制。任何一个线程都有自己的 run() 方法,要执行的方法就写在 run() 方法体内。

6. 分布式

Java 语言支持 Internet 应用的开发,在 Java 的基本应用编程接口中就有一个网络应用编程接口,它提供了网络应用编程的类库,包括 URL、URLConnection、Socket 等。Java 的 RIM 机制也是开发分布式应用的重要手段。

7. 健壮性

Java 的强类型机制、异常处理、垃圾回收机制等都是 Java 健壮性的重要保证。对指针的丢弃是 Java 的一大进步。另外,Java 的异常机制也是健壮性的一大体现。

8. 高性能

Java 的高性能主要是相对其他高级脚本语言来说的,随着 JIT(Just in Time)的发展,Java 的运行速度也越来越高。

9. 安全性

Java 通常被用在网络环境中,为此,Java 提供了一个安全机制以防止恶意代码的攻击。除了 Java 语言具有许多的安全特性以外,Java 还对通过网络下载的类增加一个安全防范机制,分配不同的名字空间以防替代本地的同名类,并包含安全管理机制。

Java虚拟机(JVM)

JVM是一个虚拟的计算机,具有指令集并使用不同的存储区域。负责执行指令,管理数据 内存 寄存器。

preview

JDK JRE JVM 关系(重点)

image-20211212195742515

编译与解释

image-20211215170531770

在dos窗口运行 如 helloJava.java 的文件

// 编译 java
javac helloJava.java
// 解释 helloJava.class
java helloJava // 这里只需要类名,切记非 java helloJava.class

源文件中代码注意事项 细是真的细!!!

  • 一个源文件中最多只能有一个被public修饰的类,而非public修饰的类可以有无数个
  • 如果一个源文件中有被public修饰的类,则该类的名字必须和文件名相同 // 区别文件(有后缀) and 文件夹 (没有后缀,只是用来存放文件的一种组织形式)
  • 主方法main(程序入口)可以放在非public修饰的类 (源文件中所有类都可以不被public修饰)
  • 一个源文件中含有多个类,通过编译会生成对应个数的字节码文件(字节码文件的名字 <==> 类名)
  • 如果一个源文件中的类与类之间没有存在联系(如下代码)可以在每一个类中放置程序入口main函数,由于最后生成不同的字节码文件,我们只需要通过java.exe工具将各个字节码放入到JVM虚拟机中进行解释执行(因为不存在同时放入两个字节码,所以不会存在程序入口冲突问题)

image-20211215173945096

class java{
	public static void main(String[] args){
		System.out.println("helloworld~");
	}
}

class person{
	public static void main(String[] args){
		System.out.println("person~");
	}
}

class animal{
	public static void main(String[] args){
		System.out.println("animal~");
	}
}

// 运行结果
C:\Users\Ushop\Desktop\mess\textJava>java java
helloworld~

C:\Users\Ushop\Desktop\mess\textJava>java person
person~

C:\Users\Ushop\Desktop\mess\textJava>java animal
animal~

转义字符

概述:\ 开头 表示接下这个字符是转义字符 比如:\n 中的n不在是字母n的含义

public static void main(String[] args){
    /* 转义字符  俗称:改变意思的字符
			\t : 一个制表符,起到对齐的功能 类似于tab的作用
			\\ : 一个\   文件路径通常表示是 C:\\Users\\Ushop\\Desktop\\mess\\textJava
			\n : 换行符
			\" : 一个"
			\' : 一个'
			\r : 一个回车,将光标移动到该行最前面,可能会引起覆盖的反作用 通常使用手法是: \r\n  == \n
				 例如"罗念笙like dog\r张洛融 ==> 显示内容:"张洛融"
	*/
    System.out.println("罗念笙\tlove\t张洛融");   // 运行结果: 罗念笙	love	张洛融
    System.out.println("\\");                    // 运行结果: \
    System.out.println("罗念笙\nlove\n张洛融");   // 运行结果:罗念笙 换行  love  换行 张洛融
    System.out.println("\"");                    // 运行结果: "
    System.out.println("\'");                     // 运行结果:'   
    System.out.println("罗念笙 like dog\r张洛融");  // 运行结果:张洛融
}

注释

单行注释 && 多行注释

注释的内容 不会被虚拟机JVM解释执行

// 单行注释
/* 
   多
   行
   注
   释
 */
文档注释

注释内容可以被JDK提供的工具javadoc所解析,生成一套以网页文件形式体现的该程序的说明文档

image-20211216211455670

/**
  * 文档注释
  * @author 罗念笙   -- 文档注释中的注解
  * @version 1.0    
  */
public class hello{
	public static void main(String[] args){
		System.out.println("helloworld~");
	}
}

/*
	笙式讲解:(下面的两行命令的区别如下图)
	        javadoc  doc文件                生成的网页文件存放的文件目录                  文档注释的java文件
		1.  javadoc   -d       C:\\Users\\Ushop\\Desktop\\mess\\textJava\\javadoc      hello.java
		    javadoc  doc文件                生成的网页文件存放的文件目录            java文件中的文档注释中所用注解(带@)  文档注释的java文件
		2.  javadoc    -d      C:\\Users\\Ushop\\Desktop\\mess\\textJava\\javadoc2   -author -version                  hello.java
		
		我使用的编码类型是GB2312  在dos命令中无出现乱码的问题
		如果你使用的是utf-8,添加  -encoding utf-8  (放在注解的位置前后)  java代码采用的是utf-8字符编码编写的
		添加 -charset utf-8       java doc html文件为utf-8字符编码
		为什么要说明是utf-8,java的编码不都是以utf-8为主的嘛?
		因为我是用的是dos命令窗口,要想显示中文得遵守GBK编码
*/
  1. 未加入注解说明

    image-20211216211217065

  2. 加入注解说明

    image-20211216211159909

javadoc 标签(文档注解)

javadoc 工具软件识别以下标签:

*标签**描述**示例*
*@author**标识一个类的作者**@author description*
*@deprecated**指名一个过期的类或成员**@deprecated description*
*{@docRoot}**指明当前文档根目录的路径**Directory Path*
*@exception**标志一个类抛出的异常**@exception exception-name explanation*
*{@inheritDoc}**从直接父类继承的注释**Inherits a comment from the immediate surperclass.*
*{@link}**插入一个到另一个主题的链接**{@link name text}*
*{@linkplain}**插入一个到另一个主题的链接,但是该链接显示纯文本字体**Inserts an in-line link to another topic.*
*@param**说明一个方法的参数**@param parameter-name explanation*
*@return**说明返回值类型**@return explanation*
*@see**指定一个到另一个主题的链接**@see anchor*
*@serial**说明一个序列化属性**@serial description*
*@serialData**说明通过writeObject( ) 和 writeExternal( )方法写的数据**@serialData description*
*@serialField**说明一个ObjectStreamField组件**@serialField name type description*
*@since**标记当引入一个特定的变化时**@since release*
*@throws**和 @exception标签一样.**The @throws tag has the same meaning as the @exception tag.*
*{@value}**显示常量的值,该常量必须是static属性。**Displays the value of a constant, which must be a static field.*
*@version**指定类的版本**@version info*

Java开发规范(重点)

image-20211218171739445

DOS命令操作(了解)

  • 讲解相对路径 VS 绝对路径(重要!!!)

  • DOS命令(大小写一样效果)

    1. 更改当前目录

      切换成其他盘(改变根目录)
      cd (chang directory) 盘符号
      例子:切换到E盘 cd: /D e:  或者 e:
      
      切换成当前盘的其他目录
      例子:cd 绝对路径 或者 相对路径
      
      切换到当前的根目录
      例子:cd\
      
      切换到上一级目录
      例子:cd ..
      
    2. 对文件夹的操作

      创建文件夹(目录)
      例子: md 文件名 (make directory)
      
      删除文件夹(目录)
      例子:rd 文件名 (remove directory)
      
      复制文件到指定文件夹(目录)
      例子: copy 复制的文件名 文件夹所在绝对路径 或者 相对路径
      
      移动文件到指定文件夹(目录)
      例子: move 移动的文件名 文件夹所在绝对路径 或者 相对路径
      
    3. 查看当前文件(目录)

      查看当前目录的文件
      dir (directory)
      例子:
      dir (查看当前目录)  
      dir 绝对路径 或者 相对路径 (查看指定目录下的文件)
      
      以一种树状图的形式展示文件结构
      用tree 代替 dir
      例子:
      tree (查看当前目录的树状图结构)
      tree 绝对路径 或者 相对路径 (查看指定目录的树状图结构)
      
    4. 其他命令

      帮助查看相关命令的使用
      例子:help cd 
      
      清屏
      例子:cls (clean screen)
      
      退出
      例子:exit
      

相关面试题

1、面向对象的特征有哪些方面? 【基础】

  • 封装

    让变量和访问这个变量的方法放在一起,将一个类中的成员变量全部定义成私有的,只有这个类自己的方法才可以访问到这些成员变量

  • 抽象

    声明方法的存在而不去实现它的类被叫做抽象类

  • 继承

    继承是子类自动共享父类数据和方法的机制,这是类之间的一种关系,提高了软件的可重用性和可扩展性

  • 多态

    多态就是指一个变量, 一个方法或者一个对象可以有不同的形式.

Java变量

变量 = 变量名 + 值 + 数据类型

数据类型(重点)

java是一种强制类型语言,每一种数据都定义了明确的数据类型,在内存中分配了不同大小的内存空间(字节)

image-20211219160638668

细节:字符串本质是类,属于引用数据类型

整数的类型
类型占用存储空间范围
byte 字节1字节-128 ~ 127
short 短整型2字节-2^15 ~ 2^15-1
int 整型4字节-2^31 ~ 2^31-1
long 长整型8字节-2^63 ~ 2^63-1

1字节(byte) = 8 位(bit)

bit 计算机中的最小的存储单位 byte 计算机中基本存储单位

浮点类型

浮点数字 = 符号位 + 指数位 + 尾数位 (尾数位可能丢失,造成精度的损失)

E38 指的是 10^38

类型占用存储空间范围
float (单精度)4字节-3.403E38 ~ 3.403E38-1
double (双精度)8字节-1.798E308 ~ 1.798E308-1

浮点数使用细节

  1. float 后面需要添加f 或 F

    float num1 = 1.0; // 错误、
    double num2 = 1.1; // 正确
    float num3 = 1.2F; // 正确
    double num4 = 1.3f; // 正确
    
  2. 浮点型常量的表示形式

    double num = 1.0; // 十进制数形式 1.0f  .512 == 0.512(小数点必须在可以省略0)
    double num5 = 5.12e2; // 科学计数法 5.12e2 == 512  5.12E-2 == 0.0512
    
  3. 通常情况下我们使用double 精度高

    double num = 1.23456789123f;
    float num1 = 1.23456789123f;
    System.out.println(num);  // 1.23456789123
    System.out.println(num1);  // 1.2345679
    
  4. 比较 小数数字是否相等时要注意:最好不要直接去比较两个浮点数是否 == 最好是两个浮点数的差值绝对值在某个精度范围内来进行比较(如0.000001)

    double num = 8.1 / 3;
    double num1 = 2.7;
    System.out.println(num); // 2.6999999999999997 
    System.out.println(num1); //2.7
    
    if(Math.abs(num - num1) < 0.000001){
        System.out.println("相等");
    }
    
字符类型
类型占用存储空间表示
char2字节表示单个字符

使用细节

  1. 字符常量是用单引号(’‘)括起来的单个字符

    char c = 'n';
    
  2. 单引号里面可以放入转义字符(虽然有两个字符,但是它是表示一个字符的 比如 // 表示 / )

    char ch = '//'
    
  3. char 的本质就是一个整数(unicode表) (**注意:字符 --> 数字 unicode 数字 --> 字符 **)

    // unicode 编码
    'a' --  97
    // char 类型字符可以进行加减 但是注意超出范围的问题
    'a' + 10 -- 107
    

字符在计算机里的存储方式

存储:‘a’ ==> 97 ==> 二进制数(11000001) ==> 存储

读取: 二进制(11000001) ==> 97 ==> ‘a’ ==> 读取

字符编码表

补充:ASCII实际上可以表示256个字符,但是只使用了128个

img

布尔类型

常用于条件判断语句 if - else while

类型占用存储空间表示
boolean1字节true / false

数据类型转换

自动类型转换

精度小的数据类型自动转换为精度大的数据类型

image-20220111140545230

自动类型转换相关的注意细节

  1. 当一个二元运算符连接两个值时,先将两个操作数转换成同一类型的数据进行计算(转换成操作数两边范围最大的数据类型)

    // 伪代码
    if(两个操作数中有一个是double类型数据){
        另一个操作数也会强制转换成double类型数据
    }else if(两个操作数中有一个是float类型数据){
        另一个操作数也会强制转换成float类型数据
    }... // 依次下去
    
  2. (byte,short)和 char 之间不会自动转换

    // 注意区分具体值赋值和变量赋值
    // 具体值赋值会先进行范围的判进行赋值
    char c = 10000; 
    short s = 'a';
    byte b = 'b';
    // 变量赋值就是依据自动类型转换或者强制类型转换
    c = (char)s;
    s = (short)c;
    
  3. 注意赋值区别(具体数值赋值和变量赋值)

    // 整数之间的赋值
    byte n = 10; // 具体数值赋值,先进行判断该数值是否在byte范围,如果在编译就不会出错,便会赋值
    int n1 = 10;
    n = n1; // int类型变量无法直接赋值给byte类型(可以进行强制类型转换)
    
    // 小数之间赋值 
    float f = 10.0; // 错误,float中将无法进行比较,本来都是一个估算值,无法进行比较范围,因此10.0是double类型无法自动转型为float
    
  4. byte,char,short 三者进行计算,在计算时首先转换成int类型数据(变量计算都会转换成int类型,不管是否是同类型数据相加减)

  5. boolean 数据类型不参与类型转换

强制类型转换

**概念:**自动类型转换的逆过程,将范围大的数据类型转换成范围小的数据类型,使用时需要在变量或者具体值前面加()

**好处:**根据程序员的意愿进行修改 **坏处:**范围超出了想要转换的数据类型,会导致溢出问题

// 例如 double ==> float
float f = (float)10.0;

强制类型转换的注意细节

  1. 强制符号只针对最近的操作数有效,往往会使用小括号来提升优先级

    int n = (int)10*3.5+12*2.3; // 错误
    int n = (int)(10*3.5+12*2.3); // 正确
    
基本数据类型和String类型的转换
  1. 将基本数据类型+"" ==> String类型

    String s = 10 + "";
    
  2. 通过基本数据类型的包装类调用parseXX方法

    String s = "2"
    Integer.parseInt(s);
    Double.parseDouble(s);
    Float.parseFloat(s);
    Short.parseShort(s);
    Long.parseLong(s);
    Boolean.parseBoolean("true");
    Byte.parseByte(s);
    
    // char 根据索引来判断
    String str = "123456789";
    char c  = str.charAt(0);
    

相关面试题

字符型常量和字符串常量的区别? (来自javaGuide)

  • 形式上: 字符常量是单引号引起的一个字符; 字符串常量是双引号引起的若干个字符

  • 含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)

  • 占内存大小 字符常量只占2个字节; 字符串常量占若干个字节(至少一个字符结束标志) (注意: char在Java中占两个字节)

String 是最基本的数据类型吗?

答:不是,String是一个类属于引用数据类型

float 型float f=3.4是否正确?

答:否,3.4 默认为double类型,如果要赋值给float 需要强制类型转换 或者后面添加f/F

short s1 = 1; s1 = s1 + 1;有什么错?

s1+1运算结果是int 型,需要强制转换类型 或者改变书写方式

三种形式都可以如下

short s1 = 1; 
s1 += 1;
s1++;
s1 = (short)(s1+1);

运算符

运算符是一种特殊的符号,用来表示数据的运算,赋值或者比较等

算术运算符

image-20220111141228314

*代码讲解

//  / 使用
10 / 4; // 2
10.0 / 4 // 2.5 
double d = 10 / 4; // 2.0

// % 使用
// 取模的本质 公式: a % b = a - a / b * b (当 a 是整数)   a % b = a - (int)a / b * b (当 a 是浮点数,但是是近似值)
10 % 3 // 1
-10 % 3 // -1
10 % -3 // 1
-10 % -3 // -1
// 总结得出取模的正负取决于被模数

关系运算符

关系运算符的结果都为boolean类型, true / false

关系运算符组成的表达式我们称为关系表达式

image-20220111141427299

instanceof 比较操作符,用于判断对象的运行类型是否为某某类型或者是某某类型的子类

逻辑运算符

用于连接多个条件(多个关系表达式),最终的值还是boolean类型

image-20220111141500121

&& 短路与:如果某一个条件为false,则后面的条件不用判断,结果为false

& 逻辑与:全部条件都要执行

|| 短路或:如果某一个条件为true,则后面的条件不用判断,结果为true

| 逻辑或:全部条件都要执行

总结: && 和 || 优于 & 和 |

习题练习

image-20220111141608640

image-20220111141632732

赋值运算符

将某个运算后的值赋给指定的变量

基本赋值运算符: =

*复合赋值运算符: += -+ = /= %=

image-20220111141720303

三元运算符

条件表达式 ?表达式1 : 表达式2

讲解:如果条件的表达式为 true ,运算后的结果表达式1,反之运行表达式2

优先级比较(补充概念:单目运算符:自加加 自减减 取反)

image-20220111154246649

标识符的命名规则和规范

笙式理解:规则即是法律,规范即是道德

概念:凡是需要自己命名的地方都是标识符(如:变量名,类名,方法名等等)

标识符的规则
  1. 组成:由26个字母大小写,0-9,- 或 $ 组成,不可以以数字为开头命名,中间不能留有空格

  2. 不可以使用关键字和保留字

    image-20220113145556828

  3. 区分大小写,长度无限制

标识符的规范

image-20220111160940752

进制转换

其他进制转换成十进制

规则:从最低位(右边)开始,将每个位上的数提取出来,乘以进制(其他进制的进制数)的(位数-1)次方,然后求和

举例:八进制转换成十进制

image-20220111163533756

十进制转换成其他进制

规则:将该数不断除以进制(其他进制的进制数),直到商为0为止,然后将每步得到的余数倒过来,就是对应的进制数

举例: 十进制转换成二进制

image-20220111163958867

位运算

原码,反码,补码

image-20220111214659288

位运算符

(&,|,^,~,>>,<<,>>>)

image-20220111232609655

详解算术左右有移动

右移 高位补0,低位舍弃,若舍弃的位=0,则相当于除以2;若舍弃的位不等于0,则会丢失精度

左移 低位补0,高位舍弃,若舍弃的位=0,则相当于**乘以2;**若舍弃的位不等于0,则会出现严重误差

负数反码移位:高位和低位都补1

负数的补码移位:右移->高位补1 ; 左移->低位补0

image-20220111232952114

// 运用补码原码知识
System.out.println(2&6); // 2
/**
 * 讲解计算机运行过程
 * 运算用补码
 * 1. 2的原码 ==> 00000000 00000000 00000000 00000010 == 2 的补码
 * 2. 6的原码 ==> 00000000 00000000 00000000 00000110 == 6 的补码
 * 运算结果为原码
 * 3. 按位与& ==> 00000000 00000000 00000000 00000010 == 6 的原码 结果为2
 */

System.out.println(~-2); // 1
/**
 * 讲解计算机运行过程
 * 运算用补码
 * 1. -2的原码 ==> 10000000 00000000 00000000 00000010
 * 2. -2的补码 ==> 11111111 11111111 11111111 11111110
 * 3. ~-2的补码 ==> 00000000 00000000 00000000 00000001
 * 运算结果为原码
 * 3. 原码结果为 00000000 00000000 00000000 00000001 == 1
 */

System.out.println(-2>>3); // -1
/**
 * 讲解计算机运行过程
 * 运算用补码
 * 1. -2的原码 ==> 10000000 00000000 00000000 00000010
 * 2. -2的补码 ==> 11111111 11111111 11111111 11111110
 * 3. -2>>3的补码 ==> 11111111 11111111 11111111 11111111
 * 4. -2>>3的反码 ==> 11111111 11111111 11111111 11111110
 * 运算结果为原码
 * 5. 原码结果为 10000000 00000000 00000000 00000001 == -1
 */

相关面试题

++ 运算符的算法题

int i = 10;
i = i++;
System.out.println(i); // 10

i = 10;
i = ++i;
System.out.println(i); // 11

编程题: 用最有效率的方法算出2 乘以8 等于几? 【基础】

答: 2 << 3

请你解释为什么会出现4.0-3.6=0.40000001这种现象?

原因简单来说是这样:2进制的小数无法精确的表达10进制小数,计算机在计算10进制小数的过程中要先转换为2进制进行计算,这个过程中出现了误差。

请你讲讲一个十进制的数在内存中是怎么存的?

补码的形式。

控制结构

控制循环类型

  • 顺序控制:程序从上到下逐行地执行,中间没有跳转和任何判断

  • 分支控制:if - else 判断

    // 单分支
    if(条件表达式){ 
        执行语句;
    }
    // 双分支
    if(条件表达式){ 
        执行语句;
    }else{
        执行语句;
    }
    // 多分支
    if(条件表达式){ 
        执行语句;
    }else if(条件表达式){
        执行语句;
    }else{
        
    }
    
  • 嵌套分支:在一个分支结构中嵌套另一层分支结构(最好不要超过三层)

  • switch分支结构

    switch(表达式){ // 表达式有具体的值 
           case 常量 1:语句一; // 表达式代表一个具体值,常量1与该值进行比较,相同则运行语句一
                      break; // 退出switch循环,否则直接进行语句2的运行(无需比较常量2)
    
           case 常量2:语句二; // 表达式代表一个具体值,常量2与该值进行比较,相同则运行语句二
                      break; 
           ....
           default: 最后一个语句; // 当不等于所有常量,则默认执行default语句   
    }
    

    细节注意:

    1. 表达式的数据类型必须和常量的数据类型必须相同 或者 可以自动转换的数据类型(常量的数据类型范围要比表达式的数据类型大)
    2. 表达式中具体值和常量的数据类型只能是(byte,short,int,char,enum,String)中的其中一个
    3. default关键字是可选择的(不是强制要求添加)
  • for循环控制 && while循环控制 && do…while循环语句

    // 基本语法
    for(循环变量初始化;循环条件;循环变量的变化){
        循环操作(可以多条语句);
    }
    运行顺序: 循环变量初始化 --> [ 循环条件 --> 循环操作 --> 循环条件的变化 ]
       
    
    // 等价于while x
    循环变量初始化;
    while(循环条件){
        循环操作(可以多条语句);
        循环变量的变化;
    }
    
    // 等价于do...while  先执行后判断
    循环变量初始化;
    do{
        循环操作(可以多条语句);
        循环变量的变化;
    }
    while(循环条件);
    

中断控制流程语句

break语句

概念:用于退出当前循环

// 不带标签的语句
for(int i = 0;i < 10;i++){
    for(int j = 0;j < 10;j++){
        break; // 默认中断当下循环
    }
}

// 带有标签的语句(格式: 标签名自定义 + :)
label1:
    for(int i = 0;i < 10;i++){
        label2:
        for(int j = 0;j < 10;j++){
            break label1; // 中断标签1的循环
        }
    }

// switch 中断语句
switch(表达式){ // 表达式有具体的值 
       case 常量 1:语句一; // 表达式代表一个具体值,常量1与该值进行比较,相同则运行语句一
                  break; // 退出switch循环,否则直接进行语句2的运行(无需比较常量2)

       case 常量2:语句二; // 表达式代表一个具体值,常量2与该值进行比较,相同则运行语句二
                  break; 
       ....
       default: 最后一个语句; // 当不等于所有常量,则默认执行default语句   
}

continue语句

概念:结束本次循环,继续进行下一次循环

// 不带标签的语句
for(int i = 0;i < 10;i++){
    for(int j = 0;j < 10;j++){
        continue; // 默认结束本次循环,而非退出循环,注意和break的区别
    }
}

// 带有标签的语句(格式: 标签名自定义 + :)
label1:
    for(int i = 0;i < 10;i++){
        label2:
        for(int j = 0;j < 10;j++){
            continue label1; // 结束label1的本次循环,跳转到label标签匹配的循环首部
        }
    }
return语句

概念:表示跳出当前所在的方法,如果该方法是main主方法,则相当于结束程序

break 和 continue 必须使用在 loop 或者 switch中,而return可以用在方法的任何位置

相关面试题

本章考验的部分一般在算法题

在JAVA 中,如何跳出当前的多重嵌套循环?【基础】

答:在最外层循环前加label 标识,然后用break:label 方法即可跳出多重循环。

swtich 是否能作用在byte 上,是否能作用在long 上,是否能作用在String上? 【基础】

答:switch(expr)中,可以传递给switch 和case语句的参数应该是int、short、char 或者byte。long不能作用于swtich,String能作用于switch上(java7以后支持)

continue、break 和 return 的区别是什么?

  1. continue :指跳出当前的这一次循环,继续下一次循环。
  2. break :指跳出整个循环体,继续执行循环下面的语句。
  3. return : 用于跳出所在方法,结束该方法的运行。return 一般有两种用法:
    • return; :直接使用 return 结束方法执行,用于没有返回值函数的方法
    • return value; :return 一个特定值,用于有返回值函数的方法

数组

一维数组

数组是引用数据类型,可以存放多个同一类型的数据,引用数据类型有默认初始值

数组的创建

// 先声明后初始化
// 声明数组名
int[] a; // 定义数组数据类型 和 数组名
或者
int a[]; // 推荐使用上面的形式 简洁明了
// 初始化数组
a = new int[5];  // 5 指的是数组长度(a.length) 

// 声明 + 初始化
int[] b = new int[5]; // 5 指的是数组长度(a.length) 
// 通过下标/索引 [0,array.length)来访问数组值 如:a[1]指的是第二个数组 

// 静态初始化  存入数组值
int[] c = {2,3,4,5,6}; // 最后一个值后面的逗号可加可不加

image-20220113194543786

注意细节

  • 数组创建后有默认初始值

    数据类型byteshortcharintlongfloatdoublebooleanString
    初始值00\u0000000.00.0falsenull
  • 数组范围,我用开区间表示:【0,array.length)

值传递和引用传递的区别

image-20220113235032216

数组拷贝 != 数组赋值(引用传递)

// Arrays方法
int[] a = {1,2,3};
int[] b = Arrays.copyOf(a,a.length)
    
// 遍历赋值
int[] a = {1,2,3};
int[] b = new int[a.length];
for(int i = 0;i < a.length;i++){
    b
}

二维数组

数组的创建

// 先声明后初始化
// 声明方式
int[][] a;   int[] a[];  int a[][];  
int[] x,y[]; // x 是一维数组 y 是二维数组
// 初始化二维数组
a[n] new int[5];  //n 的取值范围 [0,5) ,表示的是某一行的一维数组元素

// 动态初始化
int[][] twoArray = new int[5][]; // 5个一维数组,每个一维数组元素可以不相同
int[][] twoArray = new int[5][5]; // 5*5的二维数组
 
twoArray.length; // 二维数组的行数(二维数组元素个数)
twoArray[n].length; // n 的取值范围 [0,twoArray.length) ,表示的是某一行的一维数组元素

// 静态初始化
// 每个二维数组的每行元素并不一定相等
int[][] a =  {{1,2},{3,4,5,6,},{1,2,3}};
for (int i = 0; i < a.length; i++) {
    for (int j = 0; j < a[i].length; j++) {
        System.out.print("\t"+a[i][j]);
    }
    System.out.println();
}
	1	2
	3	4	5	6
	1	2	3

二维数组的内存图分析

image-20220115201337926

易错题

image-20220115204814507

多态数组

数组的定义类型为父类类型,里面保存的实际元素类型为子类类型

// 初始化多态数组
public class attay{
    public static void main(String[]args){
        // 向上转型
        // 创建5个对象: 一个人 二个学生 二个老师
        person[] p = new person[5];
        p[0] = new person();
        p[1] = new student();
        p[2] = new student();
        p[3] = new teacher();
        p[4] = new teacher();
    }
}

class person{} // 父类
class student extends person{} // 子类
class teacher extends person{}  // 子类

相关面试题

请你解释什么是值传递和引用传递?

考察点:JAVA引用传递A

值传递是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量.
引用传递一般是对于对象型变量而言的,传递的是该对象地址的一个副本, 并不是原对象本身 。 所以对引用对象进行操作会同时改变原对象.

杨辉三角实现(二维数组习题)

final int N = 10;
int[][] array = new int[N][];
for (int i = 0; i < N; i++) {
    array[i] = new int[i+1];
    for (int j = 0; j <= i; j++) {
        if(j == 0 || j == i){
            array[i][j] = 1;
        }else{
            array[i][j] = array [i-1][j] + array[i-1][j-1];
        }
    }
}

for (int i = 0; i < array.length; i++) {
    for (int j = 0; j < array[i].length; j++) {
        System.out.print(array[i][j]+"  ");
    }
    System.out.println();
}
// 实现效果
1  
1  1  
1  2  1  
1  3  3  1  
1  4  6  4  1  
1  5  10  10  5  1  
1  6  15  20  15  6  1  
1  7  21  35  35  21  7  1  
1  8  28  56  70  56  28  8  1  
1  9  36  84  126  126  84  36  9  1  

类和对象

概念:类是自定义的数据类型,对象就是一个具体的实例 <==> int 和 100 的关系

对象【属性,行为】

初识类和对象

创建对象
// 先声明后创建
Car car;
car = new Car();  // car 是对象引用,非对象本身,new Car()是对象本身

// 直接创建
Car car = new Car();
类和对象的内存图

image-20220116151322309

类的组成

属性/成员变量

概念:成员变量 = 属性 = 字段(field)

// 实例
class person{
    // 成员变量 / 属性 / 字段
    String name;
    int height;
    int weight;
    String[] friends;
}

注意细节

  • 属性格式: 访问修饰符号 + 属性类型 + 属性名

  • 属性的数据类型可以是基本数据类型或者引用数据类型

  • 属性如果不赋值,则有默认值跟数组是一样的

    image-20220116152308606

成员方法

简称:方法

// 实例
class person{
    void run(){
        System.out.println("runing..");
    }
}

/* 
格式:访问修饰符 + 返回的数据类型 + 方法名(形参列表..){
    方法体: 实现某种功能
    return 返回的数据类型:
}
*/

注意细节

  • 当程序执行到方法时候,就会开辟一个独立的空间(栈空间)
  • 返回类型可以是任何数据类型(数组或对象等等)
  • 区分实参和形参,实参是调用该方法时传入的参数,形参是在形参列表上定义的参数,需要满足:个数相同,数据类型相同或者可以自动转换,顺序对应
    • 基本数据类型传递的是值,形参的任何改变不影响实参
    • 引用数据类型传递的是地址,可以通过形参来修改实参引用的数据参数,但是无法修改引用数据类型的引用地址值
  • 方法不能嵌套使用
  • 方法的局部变量是独立的,不会受到全局变量的影响(就近原则)
递归调用

概念:方法调用它自己本身

// 例子:阶乘
class myTools{
    public int factorial(int num){
        if(num == 1){
            return num;
        }else{
            return num * factorial(num-1);
        }
    }    
}

注意细节:递归必须向退出递归的条件逼近,否则就会无限循环,最终导致栈溢出

递归习题练习(注重规律 + 条件)

// 1.斐波那契数 1 1 2 3 5 8 ...  后面的数是前面两个数之和(n > 2)
// 规律:要求的那个数 = 要求的那个数前面的数 + 要求的那个数前面的前面的数 (条件是大于2)
public int Fibonacci(int n){
    if(n == 2 || n == 1){
        return 1;
    }
    return Fibonacci(n - 1) + Fibonacci(n - 2)}

// 2.猴子吃桃 原题: 一天少一半并再多次一个,当第十天发现只剩下一个桃子,试问最初有几个桃子 
// 规律:要求的当天桃子数 = (要求的明天的桃子 + 1) / 2 (条件是桃子数不等于1)
public int peachMonkey(){
    if(peachMonkey() == 1){
        return 1;
    }
     return peachMonkey()/2-1;  
}

/**
 * 3.迷宫 8*7 (1 障碍物 0 可以通过的路)
 * 1  1  1  1  1  1  1  
 * 1  0  0  0  0  0  1  
 * 1  0  0  0  0  0  1  
 * 1  1  1  0  0  0  1  
 * 1  0  0  0  0  0  1  
 * 1  0  0  0  0  0  1  
 * 1  0  0  0  0  0  1  
 * 1  1  1  1  1  1  1
 *
 * 起点坐标(2,-2) ==> 终点坐标(6,-7)
 * 转换成数组坐标  起点坐标(1,1) ==> 终点坐标(6,5)
 */
class miGong{
    public static void main(String[] args) {

        // 创建迷宫 8*7
        int[][] map = new int[8][7];

        for (int i = 0; i < map.length; i++) {
            map[i][0] = 1;
            map[i][map[0].length-1] = 1;
        }

        for (int i = 0; i < map[0].length; i++) {
            map[0][i] = 1;
            map[map.length-1][i] = 1;
        }

        map[3][1] = 1;
        map[3][2] = 1;

        for (int i = 0; i < map.length; i++) {
            for (int j = 0; j < map[0].length;j++) {
                System.out.print(map[i][j]+"  ");
            }
            System.out.println();
        }
        System.out.println("===================");
        miGong miGong = new miGong();
        if(miGong.findWay(map,1,1)){
            for (int i = 0; i < map.length; i++) {
                for (int j = 0; j < map[0].length;j++) {
                    System.out.print(map[i][j]+"  ");
                }
                System.out.println();
            }
        }else{
            System.out.println("迷宫没有出入");
        }
    }

    /**
     * 递归方法解决走出迷宫问题
     * line , col 指的是当前的位置
     * 该方法判断当前的路能否可以走
     */
    public  Boolean findWay(int[][]map,int line,int col){
        // map 数组的各个值的含义 0 可以走的路但是没有走过 1 障碍物 2 表示走过之后可以走的路 3 表示走过,走不通的路
        if(map[6][5] == 2){
            return true;
        }else{
            // 没走过
            if(map[line][col] == 0){
                map[line][col] = 2;
                // 尝试向四个方向探索
                if(findWay(map,line - 1,col)){ // 上
                    return true;
                }else if(findWay(map,line + 1,col)){ // 下
                    return true;
                }else if(findWay(map,line,col - 1)){ // 左
                    return true;
                }else if(findWay(map,line,col + 1)){ // 右
                    return true;
                }else{
                    map[line][col] = 3;
                    return false;
                }
            }else{
                return false;
            }
        }
    }
}
// 运行效果
1  1  1  1  1  1  1  
1  0  0  0  0  0  1  
1  0  0  0  0  0  1  
1  1  1  0  0  0  1  
1  0  0  0  0  0  1  
1  0  0  0  0  0  1  
1  0  0  0  0  0  1  
1  1  1  1  1  1  1  
===================
1  1  1  1  1  1  1  
1  2  2  2  2  2  1  
1  2  2  2  2  2  1  
1  1  1  2  2  2  1  
1  3  3  2  2  2  1  
1  3  3  2  2  2  1  
1  3  3  2  2  2  1  
1  1  1  1  1  1  1  
    
// 4.汉诺塔 
class tower{
    public static void main(String[] args) {
        tower.move(64,'a','b','c');
    }

    // 方法:移动汉诺塔
    public static void move(int num,char a,char b,char c){
        // 如果只有一个盘
        if(num == 1){
            System.out.println(a +"->"+ c);
        }else{
            // 将当前的盘分成两部分,分别是:最底下一个盘以及上面的盘
            // 1.将上面的盘移到b位置借助c位置,然后将a位置最底下的盘移动到c位置
            move(num - 1,a,c,b);
            System.out.println(a +"->"+ c);

            // 2.将b位置上的盘分成两部分,分别是:最底下一个盘以及上面的盘
            // 可以将以下代码优化成:  move(num - 1,b,a,c);
            if(num == 2){
                // 2.1 如果b位置上只有一个盘,那就直接将b位置上的盘移动到c位置上
                System.out.println(b +"->" + c);
            }else{
                // 2.2 反之则将b位置上的上面的盘移动到a位置上,然后将最底下的盘移动到c位置上
                move(num - 2,b,c,a);
                System.out.println(b +"->"+ c);
                move(num - 2,a,b,c);
            }
        }
    }
}
// 运行效果
a->c
a->b
c->b
a->c
b->a
b->c
a->c
方法重载

条件:方法名必须相同 ; 形参列表类型或者数量不一致 ; 返回类型无关

比如:输出println语句

image-20220126235245517

方法重写/覆盖

概念:子类和父类~顶级类有一个方法(方法的名称,返回类型,参数)相同,那么就是子类覆盖了父类的该方法(比如 String 的 equals())

注意细节

  1. 当子类和父类~顶级类有一个方法的名称以及参数相同时就构成了 方法的重载,返回类型必须是相同的或者构成父类返回父类的类型 <> 子类返回子类的类型,否则会报错!
  2. 子类方法不能缩小父类方法的访问权限 (public > protected > 默认 > private)
// 举例
class AAA{

}

class BBB extends AAA{
    //  细节1
    public AAA method(){
        return new AAA();
    }
    //  细节2
    void method2(){}
}

class CCC extends BBB{
    //  细节1
    public BBB method(){
        return new BBB();
    }
    //  细节2
    // private void method2(){}
    public void method2(){}
}

方法重载和和重写的区别

名称发生范围方法名形参列表返回类型修饰符
重载(overload)本类必须一样类型,个数或者顺序至少有一个不同无要求无要求
重写(override)父子类必须一样必须相同返回的类型和父类返回的类型相同,或者是其子类子类方法的访问范围大于等于父类方法的访问范围
可变参数

概念:将同一个类中多个同名同功能同数据类型但参数不同的方法,封装成一个方法,就可以通过可变参数实现 (本质上就是数组形参)

格式:访问修饰符 返回数据类型 方法名(数据类型**…** 形参名)

// 求和公式
class Method{
    // 整数型
    public int add(int... nums){
        int sum = 0;
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
        }
        return sum;
    }
    // 浮点型
    public double add(double... nums){
        double sum = 0;
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
        }
        return sum;
    }
}

注意细节

  • 可变参数传入的实参可以为0或任意多个

  • 可变参数传入的实参可以为数组(实际上就是二维数组)

  • 可变参数可以和普通数据类型的参数放在参数列表中,但是必须保证可变参数放在最后

    public double add(int num,double... nums){}
    
  • 一个参数列表,可变参数的个数只能出现一个

    // public double add(int... num,double... nums){} 错误
    
作用域

主要的变量就是成员变量和局部变量(除成员变量外就是局部变量)

class person{
    // 属性(成员变量,全局变量)作用范围在至少在整个类中,具体看访问修饰符 
    // 可以不用赋值,初始值等价于数组
    int age;
    String name;
   
    {
        // 代码块中也存在局部变量
        int height;
    }
    
    void say(){
        // 局部变量 作用范围在该方法中,不过不仅仅作用于方法
        // 没有赋值则无法使用
        String content = "xcasdxc"; 
        System.out.println(content); 
    }
}

注意细节

  • 成员变量可以和局部变量重名,访问时遵循就近原则

    class scope{
        String name = "罗念笙";
        void show(){
            System.out.println("方法中:"+name);  // 罗念笙
        }
        {
            String name = "张洛融";
            System.out.println("代码块:"+name);  // 张洛融
        }
    }
    
  • 在同一作用域中,成员变量和局部变量都不能重复出现

  • 生命周期的区别,成员变量的生命周期由创建对象和销毁对象决定;局部变量的生命周期由创建代码块或者方法和销毁代码块或者方法决定

  • 修饰符不同:成员变量可以有修饰符,但是局部变量不可以添加修饰符

构造器

格式: 修饰符 方法名(形参列表){ 方法体 }

class person{
    int age;
    String name;
    
    // 构造器也称构造方法
    // 1.构造器修饰符:可以默认空,也可以 public private protected
    // 2.构造器没有返回值,也不能写void 
    // 3.方法名必须和类名一样!!!
    // 4.在创建对象时,系统会自动的调用该类的构造器完成对对象的初始化(注意:构造器非创建对象)
    public person(String name,int age){
        this.age = age;
        this.name = name;
    }
    
    // 5.如果没有有参构造器,则有一个默认的无参构造器,如果有有参构造器,则没有默认的无参构造器
    //  6.构造器重载,一个类可以有多个构造器
    public person(){}
}
this关键字

概念:jvm虚拟机会给每个对象分配this,代表当前对象

class person{
    int age;
    String name;
    
    // this可以看作该类的属性,value就是该类的地址,我们可以通过对象的hashcode() == this.hashcode()来判断
    // 哪个对象调用,这个this就是指的是哪个对象
    public person(String name,int age){
        this.age = age;
        this.name = name;
    }
}

注意细节

  • this关键字可以用来区分成员变量和局部变量(例子如上)

  • this关键字可以用来访问本类中的属性,成员方法,构造器

    class Method{
        String content = "成功!";
        // 访问构造器语句:this(参数列表)
        // 注意:this调用构造器的时候必须方法第一条语句
        public Method(){
            this("优秀!")
            System.out.println("无参构造方法");
        }
        public Method(String content){
            System.out.println("内容为:" + content);
        }
        
        void f1(){
            String con = this.content;
            System.out.println("f1方法");
        }
        
        // 访问成员方法的语法:this.方法名(参数列表);
        void f2(){
            this.f1();
            System.out.println("f2方法");
        }
    }
    
  • this不能在类定义的外部使用,只能在类定义的方法或者构造器中使用

相关面试题

1.若对一个类不重写,它的equals()方法是如何比较的?

答:比较是对象的地址。(说明String类是重写了equals()方法)

2.请解释hashCode()和equals()方法有什么联系?

  • 相等(相同)的对象必须具有相等的哈希码(或者散列码)。

  • 如果两个对象的hashCode相同,它们并不一定相同。

**详细解说:**将对象放入到集合中时,首先判断要放入对象的hashCode值与集合中的任意一个元素的hashCode值是否相等,如果不相等直接将该对象放入集合中。如果hashCode值相等,然后再通过equals方法判断要放入对象与集合中的任意一个对象是否相等,如果equals判断不相等,直接将该元素放入到集合中,否则不放入。

3.请解释Java中的概念,什么是构造函数?什么是构造函数重载?什么是复制构造函数?

当新对象被创建的时候,构造函数会被调用。每一个类都有构造函数。在程序员没有给类提供构造函数的情况下,Java编译器会为这个类创建一个默认的构造函数。
Java中构造函数重载和方法重载很相似。可以为一个类创建多个构造函数。每一个构造函数必须有它自己唯一的参数列表。
Java不支持像C++中那样的复制构造函数,这个不同点是因为如果你不自己写构造函数的情况下,Java不会创建默认的复制构造函数。

4.请说明重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?

名称发生范围方法名形参列表返回类型修饰符
重载(overload)本类必须一样类型,个数或者顺序至少有一个不同无要求无要求
重写(override)父子类必须一样必须相同返回的类型和父类返回的类型相同,或者是其子类子类方法的访问范围大于等于父类方法的访问范围

了解类和对象

IDEA开发工具的简单分布介绍

image-20220129161435477

本质就是文件夹 类的本质就是文件 package关键字

作用: 1.区分相同名字的类 2.可以很好的管理类 3.控制方法范围通过访问修饰符

包的规则和规范

包的命名规则:只能包含数组,字母,下划线,小圆点,但不能用数组开头,不能是关键字或者保留字

包的命名规范:全名都是小写字母 + 小圆点

比如 : com.公司名.项目名.业务模块名

常用包

*指的是包下所有的类都导入

image-20220129231524402

细节注意

  • package的作用是声明当前类所在的包,需要放在类的最上面(意思就是package上面除了注释什么都不能放) ,一个类最多只有一句package

  • import关键字放在package下面,在类定义前面,可以有多句,没有顺序要求

    // 一个类中最多只有一个package
    package com.al_tair.study;
    
    // import关键字放在package下面,在类定义前面,可以有多句,没有顺序要求
    import java.util.Scanner;
    import java.util.Arrays;
    
    // 类定义
    public class study{
        public static void main(String[]args){}
    }
    
访问修饰符

访问修饰符的种类

  • public : 对全部包中的类开放
  • protected : 对不同包的子类和同一个包中的类公开
  • 默认访问修饰符(无修饰符):对同一个包中的类(包含子类)公开,但是对不同包的子类不公开
  • private : 只有类本身可以访问

image-20220130203735334

使用注意细节

  • 修饰符可以用来修饰类中的属性,成员方法以及类
  • 只能用默认的和public来修饰类
面向对象编程的三大特征

封装, 继承,多态

封装

概念:就是把抽象的数据【属性】和对数据的操作【方法】封装在一起,数据被保护在内部,程序的其他部分只能通过被授权的操作【方法】,才能对数据进行操作

好处: 1.隐藏实现的细节 2.可以对数据进行验证,保证数据安全合理

封装的实现步骤

image-20220130222704836

继承

extends 关键字

格式:class 子类 extends 父类 { }

好处:解决代码复用性,当多个类中有相同的属性和方法时候,我们可以抽出相同属性和方法作为父类

image-20220201233629049

注意细节

  • 子类继承了父类所有的属性和方法,但是私有属性和方法不能被直接访问,子类和父类之间必须满足 is - a 的逻辑关系

  • 子类必须调用父类的构造器,完成父类的初始化,默认无参父类构造器,当父类没有无参构造器并且子类未用super指明对应的父类的构造器,编译不会通过

    // 父类
    public class person {
        public person() {
            System.out.println("父类构造器");
        }
    }
    
    // 子类
    public class Child extends person{
        public Child() {
            // 默认 super();
            System.out.println("子类构造器");
        }
    }
    
    class test{
        public static void main(String[] args) {
            Child child = new Child();
        }
    }
    
    // 运行结果
    父类构造器
    子类构造器
    
  • super and this 关键字使用的时候必须放在构造器的第一行,因此这两个方法调用不能共存在一个构造器

  • java所有类都是Object类的子类

  • 父类的构造器的调用不仅限于直接父类,会一直追朔直到Object类(顶级父类)

  • 子类最多只能继承一个父类(单继承机制)

内存分布图

查找属性和方法数据根据就近原则

image-20220203234708017

详解super关键字

用处:super 代表父类的引用,用于访问父类的方法,属性和构造器

注意细节:

  • 无法访问父类的私有方法和属性
  • 使用的时候必须放在构造器的第一行,因此只能调用一次父类的构造器
  • 当子类的属性,方法和父类重名时,为了访问父类的成员,必须通过super关键字来完成

this 和 super 的区别

image-20220204225246537 **

多态

概念:方法或者对象具有多种状态,多态是建立在封装和继承之上的

方法的重载和重写体现了方法的多态

对象的多态具体体现

  • 前提条件:两个对象(类)存在继承关系

  • 一个对象的编译类型和运行类型可以不一致

    // Animal 父类 Dog 子类 Cat 子类
    Animal  animal = new Dog(); // 编译类型 Aniaml 运行类型 Dog
    animal = new Cat(); // 运行类型是可以改变的
    
  • 编译类型在定义对象时候就确定了

  • 运行类型是可以改变的

  • 编译类型看定义,运行类型看 new 对象

细节分析

  • 多态的向上转型

    1. 本质:父类的引用指向子类的对象
    2. 语法:父类类型 引用名 = new 子类类型();
    3. 特点:编译类型看定义,运行类型看 new 对象
    4. 用该对象调用方法的时候,只能调用父类中的方法,不能调用子类特有的方法,因为编译不通过;但是调用方法在运行类型中是先从子类开始查找方法
    5. 属性没有重写的说法,所以属性的值看编译类型
  • 多态的向下转型

    1. 语法:子类类型 引用名 = (子类类型)父类引用;

    2. 只能强转父类的引用,不能强转父类的对象

    3. 可以调用子类类型中的所有成员

    4. 我的理解:就是让编译和运行类型上进行一个统一

      // 举例
      Animal  animal = new Dog(); // 编译类型 Aniaml 运行类型 Dog
      Dog dog = (Dog) animal; // // 编译类型 Dog 运行类型 Dog 
      
动态绑定机制

具体体现:

  • 当调用对象方法时候,该方法会和该对象的内存地址/ 运行类型进行动态绑定
  • 当调用对象的属性的时候没有动态绑定的说法

举例说明:

// 举例
public class demo{
    public static void main(String[] args){
        father f = new son();
        System.out.println(f.sum()); // 40
        System.out.println(f.sum2());  // 60
    }
}

class father{ // 父类
    public int attribute = 10;
    public int sum(){
        return getAttribute() + 10; // getAttribute()绑定的是子类的该方法
    }
    public int sum2(){
        return attribute + 50; // 属性没有动态绑定机制
    }
    public int getAttribute(){
        return attribute;
    } 
    
}

class son extends father{ // 子类
    public int attribute = 30;
    public int getAttribute(){
        return attribute;
    }
}
Object类详解

image-20220205225527879

equals

equals方法 和 == 比较运算符 的区别

== :如果判断基本数据类型,判断值是否相同;如果判断引用数据类型,判断的是地址是否相同(是否是 同一个对象)

equals:是Object类中的方法,只能用来判断引用类型,默认是判断地址是否相同,但是像String类重写了该方法,用来判断字符串值是否相同

// Object类的源码
public boolean equals(Object obj) {
    return (this == obj);
}

// String类的源码
   /**
     * Compares this string to the specified object.  The result is {@code
     * true} if and only if the argument is not {@code null} and is a {@code
     * String} object that represents the same sequence of characters as this
     * object.
     */
    public boolean equals(Object anObject) {
        if (this == anObject) { // 比较地址是否相同
            return true;
        }
        if (anObject instanceof String) { // 判断是否为String 或者 String的父类
            String aString = (String)anObject; // 向下转型:目的是为了获得String类的属性和方法
            if (!COMPACT_STRINGS || this.coder == aString.coder) {
                return StringLatin1.equals(value, aString.value);
            }
        }
        return false;
    }

// StringLatin1类的源码 底层就是比较字符数组中每个字符是否相同
 @HotSpotIntrinsicCandidate
    public static boolean equals(byte[] value, byte[] other) {
        if (value.length == other.length) {
            for (int i = 0; i < value.length; i++) {
                if (value[i] != other[i]) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }
hashCode

作用:返回该对象的哈希码值,是为了提高哈希表的性能

注意细节

  • 提高具有哈希结构的容器的效率
  • 两个引用都指向同一个对象,则哈希值一定相同
  • 哈希值主要是根据地址来的,将对象的内部地址转换成一个整数来实现的
toString

默认返回:全类名 + @ + 哈希值十六进制

作用:用于返回该对象的属性信息

// Object源码
// java.lang.Object@16b98e56
public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

注意细节

  • 当直接输出一个对象时,toString方法会被默认的调用

    Object o = new Object();
    System.out.println(o.toString()); // java.lang.Object@16b98e56
    System.out.println(o); //java.lang.Object@16b98e56
    
finalize

概念:当垃圾回收器确定不存在该对象的更多引用时,由对象的垃圾回收器调用此方法

注意细节

  • 当某个对象没有任何引用时,则jvm虚拟机就会来销毁该对象,在销毁该对象前,就会调用该对象的finalize方法

    public class person {
    	public person() {}
        // 该方法已经被废除,不推荐使用
        @Override
        protected void finalize() throws Throwable {
            System.out.println("我已经被销毁了...");
        }
    }
    
    class test{
        public static void main(String[] args) {
            new person();
            System.gc(); // 运行垃圾回收器
        }
    }
    
    // 显示效果:我已经被销毁了...
    
  • 垃圾回收机制的调用,由系统来决定,我们可以通过System.gc() 主动触发垃圾回收机制

断点调试
断点调试(idea)默认快捷键
  • F7:跳入方法
  • F8:逐行执行代码
  • shift+F8:跳出方法
  • F9:执行到下一个断点
Idea debug如何进入 Jdk源码
解决方法1

使用force step into : 快捷键 alt + shift + F7

解决方法2

这个配置一下就好了:
点击Setting --> Build,Execution,Deployment --> Debugger --> Stepping
把Do not step into the classes中的java.*,javax.*取消勾选

img

相关面试题

1.请说明一下final, finally, finalize的区别

final 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。
finally是异常处理语句结构的一部分,表示总是执行。
finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源
回收,例如关闭文件等。

2.请说明面向对象的特征有哪些方面

  1. 抽象:抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只
    是选择其中的一部分,暂时不用部分细节。抽象包括两个方面,一是过程抽象,二是数据抽象。
  2. 继承:继承是一种联结类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法。对象的一个新类可以从现有的类中派
    生,这个过程称为类继承。新类继承了原始类的特性,新类称为原始类的派生类(子类),而原始类称为新类的基类(父类)。派生
    类可以从它的基类那里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要。
  3. 封装:封装是把过程和数据包围起来,对数据的访问只能通过已定义的界面。面向对象计算始于这个基本概念,即现实世界可以被描绘成一
    系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。
  4. 多态性:多态性是指允许不同类的对象对同一消息作出响应。多态性包括参数化多态性和包含多态性。多态性语言具有灵活、抽象、行为共享
    、代码共享的优势,很好的解决了应用程序函数同名问题。

3.请列举你所知道的Object类的方法并简要说明。

Object()默认构造方法。clone() 创建并返回此对象的一个副本。equals(Object obj) 指示某个其他对象是否与此对象“相等”。finalize()当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。getClass()返回一个对象的运行时类。hashCode()返回该对象的哈希码值。 notify()唤醒在此对象监视器上等待的单个线程。 notifyAll()唤醒在此对象监视器上等待的所有线程。toString()返回该对象的字符串表示。wait()导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。wait(long timeout)导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量。wait(long timeout, int nanos) 导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。

深入类和对象

类变量和类方法

类变量也称作静态变量,类方法也称作静态方法

与成员变量的区别就是有无 static 关键字

注意细节

  1. 静态变量是同一个类所有对象共享
  2. 类变量在类加载的时候就生成了
  3. 类变量中不能使用和对象有关的关键字(比如:,this,super ),因为this或者super的产生需要创建对象,但是类变量在类加载的时候就出现了
  4. 类方法只能访问类变量或者类方法,但是反之普通成员方法既可以访问非静态成员,也可以访问静态成员
  5. 类变量的生命周期随着类的加载开始,随着类消亡而销毁
  6. 如果父类中含有一个静态方法,且在子类中也含有一个返回类型、方法名、参数列表均与之相同的静态方法,那么该子类实际上只是将父类中的该同名方法进行了隐藏,而非重写。换句话说,父类和子类中含有的其实是两个没有关系的方法,它们的行为也并不具有多态性

语法:访问修饰符 static 数据类型 变量名; (访问修饰符和static的顺序可以交换)

访问方式: 类名.类变量(推荐) 对象名.类变量名

// 举例类变量和类方法的使用
// Math类的源码
 public static final double PI = 3.14159265358979323846;
// 取绝对值
@HotSpotIntrinsicCandidate
public static int abs(int a) {
    return (a < 0) ? -a : a;
}
Main方法

解释main方法的形式; pubLic static void main(String[]args)

public class HelloMain {
    /*
     * 1.main方法时虚拟机调用
     * 2.java虚拟机调用类的main方法,所以该方法的访问权限必须是public
     * 3.java虚拟机在执行main()方法时不必创建对象,所以用static
     * 4.Java执行程序 参数1 参数2 参数3【举例说明: java HelloMain hello main !】 参数1:hello 参数2:main 参数3:!
     *
     * 特别提示:
     * 1.在main()方法中,我们可以直接调用main方法所在的类的静态方法和静态属性
     * 2.但是,不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象访问非静态成员
     */
    public static void main(String[]args){
        // args 是如何传入 在idea上设置 Run ->  Edit Configurations  ->  program arguments:
        // 遍历显示
        for(int i=0;i<args.length;i++){
            System.out.println("第"+(i+1)+"个参数"+args[i]);
        }
    }
}
代码块
普通代码块

普通代码块又称初始化块,属于类中的成员部分,没有方法名,没有返回,没有形参,只有方法体

普通代码块不需要被调用,而是加载对象的时候隐式调用

语法:{ // 方法体 }; 没有修饰符 ;分号可有可无

public class CodeBlock {
    /*
     * 代码块
     * 1.相当于另外一种形式的构造器(对构造器的补充机制),可以做初始化的操作
     * 2.场景:如果多个构造器中都有重复的语句,可以抽取到初始化块中,提高代码的重用性
     */
    public static void main(String[]args){
        new Movie("爱情神话");
    }
}
class Movie{
    private String name;
    private double price;
    private String director;

    /*
     * 下面的三个构造函数都有相同的语句,这样看起来重复
     * 代码块的使用就可以很好的解决这个问题
     */
    {
        System.out.println("电影开始~~");
    };
    public  Movie(){}
    public Movie(String name){
//        System.out.println("电影开始~~");
        this.name = name;
    }
    public Movie(String name,double price){
//        System.out.println("电影开始~~");
        this.name = name;
        this.price = price;
    }
    public Movie(String name,double price,String director){
//        System.out.println("电影开始~~");
        this.name = name;
        this.price = price;
        this.director = director;
    }
}

注意细节

  • 普通代码块是随着对象的创建而执行的,有几个对象就运行几次代码块
  • 如果不创建对象,只是使用类的静态成员,不会执行代码块
静态代码块

静态代码块,属于类中的成员部分,没有方法名,没有返回,没有形参,只有方法体和修饰符static

静态代码块不需要被调用,而是加载类的时候隐式调用

语法:static{ // 方法体 }; ;分号可有可无

类什么时候被加载?【重点】

  1. 创建对象化实例时,并且父类的也会被加载
  2. 使用类的静态成员时(静态属性,静态方法)
  3. 使用子类的静态成员时

静态代码块的例子

public class StaticCodeBlock {
    /*
     * 父类和子类的静态代码块和普通代码块执行顺序是什么呢?
     * 在父类和子类都有静态代码块和普通代码块时候,创建子类实例时候的执行顺序: 父类静态代码块 --> 子类静态代码块 --> 父类普通代码块 --> 子类普通代码块
     * 在父类和子类都有静态代码块和普通代码块时候,创建父类实例时候的执行顺序: 父类静态代码块 --> 父类普通代码块
     * 在父类和子类都有静态代码块和普通代码块时候,使用父类的静态成员时只调用父类的静态代码块
     * 在父类和子类都有静态代码块和普通代码块时候,使用子类的静态成员时候的执行顺+序: 父类静态代码块 --> 父类普通代码块
     *
     * 静态属性初始化,静态代码块,静态方法,构造器的优先级
     * 1.父类的静态属性初始化优先级 == 静态代码块优先级 (看代码的执行顺序)
     * 2.子类的静态属性初始化优先级 == 静态代码块优先级 (看代码的执行顺序)
     * 3.父类的成员变量初始化优先级 == 普通代码块优先级 (看代码的执行顺序)
     * 4.父类的构造器
     * 5.子类的成员变量初始化优先级 == 普通代码块优先级 (看代码的执行顺序)
     * 6.子类的构造器
     * 方法都需要调用才会执行
     * public 构造函数名(){
     *    1)super()  
     *    2)普通代码块
     *    3)构造函数内容
     * }
     */
    public static void main(String[]args){
        // 使用类的静态成员时(静态属性,静态方法)
        // AA.name = "念笙";
        // AA.show();

        // 创建对象实例时(new)
        // new AA();

        // 创建子类的对象实例,父类也会被加载
        // new BB();

        // 使用子类的静态成员时(静态属性,静态方法)
        // BB.sex = "男";
        // BB.show();

        // 静态初始化,静态代码块,静态方法的优先级
           new CC();
        //   CC.show();
    }
}

class AA{
    public static String name;

    static {
        System.out.println("AA静态代码块被调用");
    }
    {
        System.out.println("AA普通代码块被调用");
    }
    public static void show(){
        System.out.println("name:"+name);
    }
}

class BB extends AA{
    public static String sex;
    static {
        System.out.println("BB静态代码块被调用");
    }
    {
        System.out.println("BB普通代码块被调用");
    }
    public static void show(){
        System.out.println("sex:"+sex);
    }
}

class CC{
    public CC(){
        // 1)super()
        // 2)普通代码块
        System.out.println("构造器被调用");
    }
    // 静态属性初始化
    private static int age = getAge();

    // 静态方法
    public static void show(){
        System.out.println("age:"+age);
    }

    // 静态代码块
    static{
        System.out.println("CC静态代码块被调用");
    }

    public static int getAge(){
        System.out.println("age:"+age);
        return 18;
    }
}
final关键字

final 关键字可以用来修饰类,属性,方法和局部变量 不能修饰构造器

public class Final {
    /*
     * 使用final的情况
     * (1)当不希望类被继承时,可以用final修饰
     * (2)当不希望父类的某个方法被子类覆盖或者重写
     * (3)当不希望类的某个属性的值被修改,可以用final修饰 [例如:final double PI = 3.1415926] -> 常量
     * (4)当不希望某个局部变量被修改,可以用final修饰 -> 局部常量
     *
     * 使用final关键字的细节讨论
     * 1.final修饰的属性又叫常量,一般用 XXX_XXX_XXX来命名
     * 2.final修饰的属性在定义时,必须赋初始值,赋值可以加在如下的位置上
     *    属性定义时初始化 / 在构造器中 / 在代码块中
     *   final修饰的属性是静态的,则,赋值可以加在如下的位置上
     *    静态属性定义时初始化 / 在静态代码块中
     * 3.final类不能继承,但是可以实例化对象
     * 4.如果类不是final类,但是含有final方法,虽然该方法不能重写,但是该类可以被继承
     * 5.final 和 static 往往搭配使用效率更高,不会导致类加载,底层编译器做了优化 如下例子
     * 6.包装类(Integer,double,Float,Boolean等)不能继承
     */
    public static void main(String[] args) {
        /*
         * final 修饰基本数据类型
         * 基本数据类型的值不能发生变化
         */
        final int age = 30;
        // sge = 100; 报错

        /*
         * final修饰引用数据类型
         * 引用数据类型饿地址值不能发生改变,但是该地址上的内容可以发生改变
         */
        final FU fu = new FU();
        // fu.show4();
        // fu = new FU();  报错
        
        // final 和 static 往往搭配使用效率更高,不会导致类加载,底层编译器做了优化
        System.out.println(BB.name);
    }
}

class AA{
   // 静态属性定义时初始化
    public static final int NUMS1 = 12;
    public final int NUM1 = 12;

    public static final int NUMS2;
    public final int NUM2;

    public static final int NUMS3;
    public final int NUM3;
    
    // 在静态代码块中初始化
    static{
        NUMS2 = 12;
        NUMS3 = 12;
    }
    {
        NUM3 = 12;
    }
    // 在构造器中初始化
    public AA(){
        NUM2 = 12;
    }
}

// final 和 static 往往搭配使用效率更高,不会导致类加载,底层编译器做了优化
class BB{
    public static final String name = "yyds";
    static {
        System.out.println("类加载了");
    }
    // 运行效果:不会显示类加载了 说明类没有被加载
}

抽象类

引出:当父类的某些方法需要申明,但又不确定如何实现的时候,可以把这个类变成抽象类 添加 abstract关键字

抽象类的本质还是类可以有方法,属性,代码块,构造器,但是抽象类不能被实例化

语法:

  • 类:访问修饰符 abstract class 类名{}
  • 方法:访问修饰符 abstract 返回类型 方法名(参数列表); // 没有方法体
public class AbstractDemo {
    /*
     * 抽象类 ==> 简化父类方法的不确定代码
     * 抽象类会被继承,由自己的子类实现方法
     *
     * 注意事项: 
     * 1.抽象类不能被实例化,也不能直接通过类名.静态属性的方法来访问抽象类中的静态属性,可以被子类用super关键字调用构造器
     * 2.抽象方法对应抽象类或者接口;但是有抽象类,不仅可以没有抽象方法还可以有方法体的普通方法
     * 3.abstract 只能修饰类和方法
     * 4.如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法,除非它自己也声明为abstract,但是如果子类实现抽象类的全部抽象方法,则子类的子类就不需要      *   实现了抽象方法
     * 5.抽象方法不能使用private,final和static来修饰,因为这些关键字都是和重写相违背的
     */
    public static void main(String[] args) {
        // 抽象类不能被实例化
        // Animal animal = new Animal(); 报错
    }
}

// 当一个类中存在抽象方法时候,需要将该类声明成为abstract类
abstract class  Animal{
    public String name;
    // 思考:有意义吗??  ==> 方法不确定性 ==> 考虑设计成抽象类方法(abstract)即是没有方法体 ==> 让子类继承实现
    //    public void eat(){
    //        System.out.println("吃撒");
    //    }
    public abstract void eat();
}
接口

概念:接口在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明。一个类通过继承接口的方式,从而来继承接口的抽象方法。

语法:访问修饰符 interface 接口名{} 访问修饰符:public 和默认 ; class 类名 implements 接口{ // 必须实现的抽象方法 }

注意细节

  • 在jdk7之前都是没有方法体的抽象方法,在jdk8以后接口的方法可以有具体实现,但是必须是静态方法或者是默认方法

    // 接口中的所有方法都是public修饰的方法,接口中的抽象方法可以不用被abstract修饰
    // 可以通过接口名.静态方法名来调用静态方法 接口名.属性名来访问属性
    interface Interface{
        // 属性 默认是public  static final修饰
        int n1 = 10;
    
        // 抽象方法  默认是public abstract
         void method1();
    
        // 默认方法 添加default关键字  默认是public 
        default public void method2(){
            System.out.println("默认方法");
        }
    
        // 静态方法
        public static void  method3(){
            System.out.println("静态方法");
        }
    } 
    
  • 接口不能被实例化

  • 一个非抽象类实现了该接口就必须实现该接口上的所有抽象方法,抽象类去实现接口时候,可以不用实现接口的抽象方法

  • 一个类同时可以实现多个接口,一个接口不能继承其他的类,但是可以继承多个其他接口

    // 顶级接口
    interface top{}
    
    // 父级接口  一个接口不能继承其他的类,但是可以继承多个其他接口
    interface secondTop extends top{}
    
    //另外一个接口
    interface another{}
    
    // 实现类 一个类同时可以实现多个接口
    class imp implements secondTop,another{}
    

实现接口 Vs 继承类

  1. 接口和继承解决的问题不同
    继承的价值主要在于:解决代码的复用性和可维护性
    接口的价值主要在于:设计好各种规范(方法),让其他类去实现这些方法

  2. 接口比继承更加灵活
    继承满足is-a的关系,而接口只需满足like-a的关系

  3. 接口在一定程度上实现代码解耦[接口的规范性+动态绑定]

抽象类和接口的区别

  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。
  • 一个类只能继承一个抽象类,而一个类却可以实现多个接口。
  • 抽象可以有普通方法(含有方法体),接口只能是默认方法(带有default关键字)

接口的多态性

// Usb接口
public interface UsbInterface {  
    // 规定相关接口的方法
    public void start();
    public void end();
}

// 相机实现了该接口
public class Camera implements UsbInterface{ 
    @Override
    public void start() {
        System.out.println("照相机开始运行了~");
    }

    @Override
    public void end() {
        System.out.println("照相机开始关机了~");
    }
}

// 手机实现了该接口
public class Phone implements UsbInterface{ // 实现接口
    @Override
    public void start() {
        System.out.println("手机开始运行了~");
    }

    @Override
    public void end() {
        System.out.println("手机开始关机了~");
    }
}

// 接口插入,分别实现不同功能
public class Computer {
    // 计算机工作
    public  void work(UsbInterface usbInterface){
        usbInterface.start();
        usbInterface.end();
    }
}

public class Interface01 {
    /*
     * 接口多态特性
     * 1)既可以接收手机对象,又可以接受相机对象,就体现了接口多态(接口引用可以指向实现了该接口的类的对象)
     * 2)多态数组
     * usbs[i] instanceof Phone 向下转型,判读Usb接口是否为Phone
     * 接口类型的变量可以指向实现了该接口的类的对象的实例
     */
    public static void main(String[]args){
        // 创建手机和相机对象
        Camera camera = new Camera();
        Phone phone = new Phone();
        // 创建电脑对象
        Computer computer = new Computer();
        // 插入接口,相机和手机分别运作
        computer.work(phone);
        computer.work(camera);
        // 多态数组
        UsbInterface[] UsbS = new UsbInterface[2]; 
        UsbS[0] = new Phone(); 
        UsbS[1] =  new Camera();
    }
}

内部类

概念:一个类的内部嵌套了另一个类结构,被嵌套的类称为内部类,嵌套其他类的类称为外部类(注意类的五大成员:属性,方法,构造器,代码块,内部类)

class OuterClass{ // 外部类
    int n1 = 100; // 属性
    public OuterClass(int n3){} // 构造方法
    void n2(){} //方法
    {} // 代码块
    class InnerClass{} // 内部类
}

语法:class Outer{ // 外部类 class Inner{ // 内部类 } }

内部类的分类:

  • 定义在外部类局部位置(比如方法内)
    • 局部内部类(有类名)
    • 匿名内部类(没有类名,重点)
  • 定义在外部类的成员位置上
    • 成员内部类(没有static修饰)
    • 静态内部类(有static修饰)
局部内部类

局部内部类是定义在外部类的局部位置(比如:方法中),并且有类名

作用域:仅在定义它的方法或代码块中

class OuterClass02{ // 外部类
    private int n1 = 100;
    public void n2(){
        /*
         * 局部内部类
         * 1.局部内部类是定义在外部类的局部位置,通常在方法里或者代码块里
         * 2.局部内部类可以访问外部类的所有成员变量,包含私有的
         * 3.局部内部类不能添加访问修饰符,但是是可以被final修饰,可以不被继承
         * 4.作用域:仅在定义它的方法或代码块中
         * 5.外部类在方法中可以创建InnerPartClass对象,然后调用局部内部类的方法,不允许在方法外创建该对象
         * 6.如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果就想访问外部类的成员,则可以使用(外部类名.this.成员)
         */
        class InnerPartClass{ 
            private int n1 = 20;
            public void n3(){
                // 7.如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.this.成员)
                System.out.println(n1); // 20
                // 解读OuterClass02.this 本质就是外部类的对象即那个对象调用了n2方法,OuterClass02.this就是那个对象,这里是out对象调用的,因此					  OuterClass02.this指向out对象
                System.out.println(OuterClass02.this.n1); // 100
                System.out.println(OuterClass02.this); // com.Al_tair.innerClass_.OuterClass02@27d6c5e0
            }
        }
        // 5.外部类在方法中可以创建InnerPartClass对象,然后调用局部内部类的方法
        InnerPartClass innerPartClass = new InnerPartClass();
        innerPartClass.n3();
    }
}

public class InnerClass01 { // 外部其他类
    public static void main(String[] args) {
        OuterClass02 out = new OuterClass02();
        out.n2();
        System.out.println(out); // com.Al_tair.innerClass_.OuterClass02@27d6c5e0
    }
}
匿名内部类

概念:定义在外部类的局部位置,没有类名

作用域:仅仅在定义它的方法和代码块中

语法:new 类名或接口名(参数列表){ // 匿名内部类 }

public class AnonymousInnerClass {
    /*
     * 匿名内部类
     * 定义在外部类的局部位置<=>局部内部类(没有表露出来的类名)
     * 基本语法:new 类或接口(参数列表){}
     */
    public static void main(String[] args) {
        OutClass outClass = new OutClass();
        outClass.meathod();
    }
}

class OutClass{
    private int n = 10;
    public void meathod(){
        /*
         * 使用匿名内部类简化开发
         * aa的编译类型 -- AA ; aa的运行类型 -- 匿名内部类OutClass$1 (外部类名$匿名内部类的序号)
         * 1.创建匿名内部类后马上创建该实例,并返回该地址给aa
         * 2.匿名内部类只能使用一次,但是该实例对象可以反复引用,就是不能用一个匿名内部类创建多个对象实例
         * 3.不能添加访问修饰符,因为它的就是一个局部变量
         * 4.如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.this.成员)
         */
        // 第一种调用匿名内部类的内部方法
        // 这里new的不是接口实例化,而是接口的匿名内部实现类
        AA aa = new AA(){ // 向上转型
            @Override
            public void cry() {
                System.out.println("嘤嘤嘤~~");
            }
        };
        aa.cry();
        System.out.println(aa.getClass()); // class com.Al_tair.innerClass_.OutClass$1

        // 第二种调用匿名内部类的内部方法
        System.out.println(
            new AA(){
                @Override
                public void cry() {
                    System.out.println("555~~");
                }
            }.getClass()
        ); // class com.Al_tair.innerClass_.OutClass$2
    }
}

interface AA{
    void cry();
}

实践

// 对比传统方式和匿名内部类的区别
public class Test {
    public static void main(String[] args) {
        // 当作实参直接传递,简洁高效
        show(new Paint() {
            @Override
            public void show() {
                System.out.println("最美油画!");
            }
        });

        // 传统方式
        new Picture().show();
    }

    public static void show(Paint paint){
        paint.show();
    }
}

// Paint接口
interface Paint{
    public void show();
}

class Picture implements Paint{
    @Override
    public void show() {
        System.out.println("最美油画!");
    }
}
成员内部类

概念:成员内部类是定义在外部类的成员位置,并且没有static修饰

作用域: 可以直接访问外部类的所有成员

public class MemberInnerDemo01 {
    public static void main(String[] args) {
        // 成员内部类的访问方式、
        // 1.创建外部类的对象来调用
        //   1.创建外部类的对象
        MemberOuterClass memberOuterClass = new MemberOuterClass();
        //   2.创建成员内部类的对象 需要加外部类名 (如: 外部类名.内部类名 对象引用名 = 外部类的对象名.new 内部类名();)
        MemberOuterClass.InnerClass innerClass = memberOuterClass.new InnerClass();

        // 2.调用外部类方法访问成员内部类
        MemberOuterClass.InnerClass innerClass2 = new MemberOuterClass().getInnerInstance();
    }
}

class MemberOuterClass{
    /*
     *  成员内部类(没有static修饰)
     *  1.作用域: 可以直接访问外部类的所有成员
     *  2.可以添加任意的访问修饰符: public 默认 protected private
     *  3.定义在外部类的成员位置上
     *  4.成员内部类的访问方式
     *    1.成员内部类 —> 外部类成员 [直接访问]
     *    2.外部类 -> 成员内部类 [创建成员内部类的对象,再访问]
     *    3.其他外部类 -> 成员内部类 [创建外部类的对象,创建成员内部类的对象,再访问如上第一点]
     *  5.如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.this.成员)
     */
    protected class InnerClass{}
    public InnerClass getInnerInstance(){
        return new InnerClass();
    }
}
静态内部类

概念:成员内部类是定义在外部类的成员位置,并且有static修饰

作用域: 可以直接访问内部类的所有静态成员,包含私有的,但是不能访问非静态成员

public class StaticInnerClass {
    public static void main(String[] args) {
        StaticOuterClass.StaticInner staticClass = new StaticOuterClass.StaticInner();
        staticClass.say();
    }
}

class StaticOuterClass{
   /*
    * 静态内部类 static修饰
    * 1.可以直接访问内部类的所有静态成员,包含私有的,但是不能访问非静态成员
    * 2.可以添加任何的访问修饰符: public 默认 protected private
    * 3.静态内部类的访问方式(2和3创建静态内部类的对象写法不一样)
    *   1.静态内部类 -> 外部类 [直接访问所有静态成员]
    *   2.外部类 -> 静态内部类 [创建静态内部类的对象,再访问]
    *   3.其他外部类 -> 静态内部类 [创建静态内部类的对象,再访问]
    *  4.如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.成员)
    */
    static class StaticInner{
        public  void say(){
            System.out.println("StaticInner 在 saying");
        }
    }
}

相关面试题

1.请说明Java的接口和C++的虚类的相同和不同处

由于Java不支持多继承,而有可能某个类或对象要使用分别在几个类或对象里面的方法或属性,现有的单继承机制就不能满足要求。
与继承相比,接口有更高的灵活性,因为接口中没有任何实现代码。当一个类实现了接口以后,该类要实现接口里面所有的方法和属性,并且接口里面的属性在默认状态下面都是public static,所有方法默认情况下是public,一个类可以实现多个接口。

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

  1. 接口中所有的方法隐含的都是抽象的。而抽象类则可以同时包含抽象和非抽象的方法。
  2. 类可以实现很多个接口,但是只能继承一个抽象类
  3. 类可以不实现抽象类和接口声明的所有方法,当然,在这种情况下,类也必须得声明成是抽象的。
  4. 抽象类可以在不提供接口方法实现的情况下实现接口。
  5. Java接口中声明的变量默认都是final的。抽象类可以包含非final的变量。
  6. Java接口中的成员函数默认是public的。抽象类的成员函数可以是private,protected或者是public。
  7. 接口是绝对抽象的,不可以被实例化。抽象类也不可以被实例化,但是,如果它包含main方法的话是可以被调用的。

枚举和注解

概念:枚举是一种特殊的类,里面只包含一组有限的特定的对象

自定义枚举

// 案例:创建春夏秋冬四个季节
public class EnumClass01 {
    public static void main(String[] args) {
        System.out.println(EnumDemo.WINTER);
        System.out.println(EnumDemo.SUMMER);
        System.out.println(EnumDemo.AUTUMN);
        System.out.println(EnumDemo.SPRING.toString());
    }
}
/*
 * 自定义枚举 (枚举对象名通常为大写字母)
 * 1)构造器私有化 => 外部不能创建对象
 * 2)去掉setXxx方法 => 外部不能修改属性
 * 3)直接创建固定的对象 => 枚举的特定用法
 * 4)可以使用 final static 修饰符优化 => 防止类初始化的时候加载对象
 * 5)可以提供get方法,重写toString方法用来输出
 */
class EnumDemo{
    private String seasonName;
    private String seasonFeature;

    public String getSeasonFeature() {
        return seasonFeature;
    }

    public String getSeasonName() {
        return seasonName;
    }

    //构造器
    private EnumDemo(String seasonName,String seasonFeature){
        this.seasonName = seasonName;
        this.seasonFeature = seasonFeature;
    }
    // 创建枚举对象
    final static EnumDemo SPRING = new EnumDemo("春天","温暖");
    final static EnumDemo SUMMER = new EnumDemo("夏天","炎热");
    final static EnumDemo AUTUMN = new EnumDemo("秋天","凉爽");
    final static EnumDemo WINTER = new EnumDemo("冬天","寒冷");

    @Override
    public String toString() {
        return "["+getSeasonName()+"--->"+getSeasonFeature()+"]";
    }
}

enum关键字实现枚举

public class EnumClass02 {
    public static void main(String[] args) {
        System.out.println(Session.AUTUMN);
    }
}
/*
 * 使用enum关键字来实现枚举
 * 1.当我们使用enum关键字开发一个枚举类时,默认会继承Enum类  java.lang.Enum<com.Al_tair.enum_.Session>
 * 反编译javap Session.class:
 * final class com.Al_tair.enum_.Session extends java.lang.Enum<com.Al_tair.enum_.Session> {
 *   public static final com.Al_tair.enum_.Session SPRING;
 *   public static final com.Al_tair.enum_.Session SUMMER;
 *   public static final com.Al_tair.enum_.Session AUTUMN;
 *   public static final com.Al_tair.enum_.Session WINTER;
 *   public static com.Al_tair.enum_.Session[] values();  // 隐藏
 *   public static com.Al_tair.enum_.Session valueOf(java.lang.String); 
 *   public java.lang.String getSeasonFeature();
 *   public java.lang.String getSeasonName();
 *   public java.lang.String toString();
 *   static {};
 * }
 */
enum Session{
    /*
     * Enum枚举类
     * 1.使用关键字enum替代class
     * 2.格式: 形式常量名(实参列表)
     * 3.如果有多个常量对象,使用,号间隔
     * 4.如果使用enum枚举,要求将定义常量对象写在最前面
     * 5.  
     * 6.如果我们是使用无参构造器的时候,创建常量对象可以省略括号
     */
    SPRING("春天","温暖"),
    SUMMER("夏天","炎热"),
    AUTUMN("秋天","凉爽"),
    WINTER("冬天","寒冷");

    private String seasonName;
    private String seasonFeature;

    public String getSeasonFeature() {
        return seasonFeature;
    }

    public String getSeasonName() {
        return seasonName;
    }

    //构造器
    private Session(String seasonName,String seasonFeature){
        this.seasonName = seasonName;
        this.seasonFeature = seasonFeature;
    }

    @Override
    public String toString() {
        return "["+getSeasonName()+"--->"+getSeasonFeature()+"]";
    }
}

枚举常用方法

image-20220221102947758

 // 测试枚举常用方法
enum Week{
    MONDAY("星期一"),TUESDAY("星期二"),WEDNESDAY("星期三"),THURSDAY("星期四"),FRIDAY("星期五"),SATURDAY("星期六"),SUNDAY("星期日");

    private String WeekName;

    private Week(String WeekName) {
        this.WeekName = WeekName;
    }
}

class testMethod{
    public static void main(String[] args) {
        Week f = Week.FRIDAY;
        // name() 获得枚举常量对象名;优先使用toString()
        System.out.println(f.toString()); // FRIDAY
        System.out.println(f.toString() == f.name()); // true

        // ordinal() 得到当前枚举常量对象的次序 索引 0~
        System.out.println(f.ordinal()); // 4

        // values();  // 隐藏,Enum类中无法查看 本质: 包含所有常量对象的数组
        // 实现效果: MONDAY  TUESDAY  WEDNESDAY  THURSDAY  FRIDAY  SATURDAY  SUNDAY
        for (int i = 0; i < Week.values().length; i++) {
            System.out.print(Week.values()[i]+"  ");
        }
        System.out.println();

        /* valueOf()方法 将字符串转换成枚举对象,要求字符串必须为已有的常量对象名,否则博报错
         * 源码如下
         *     public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) {
         *         T result = enumType.enumConstantDirectory().get(name);
         *         if (result != null) return result;
         *         if (name == null) throw new NullPointerException("Name is null");
         *         throw new IllegalArgumentException("No enum constant " +
         *                      enumType.getCanonicalName() + "." + name);
         *     }
         */
        Week m = Week.valueOf("MONDAY");
        System.out.println(m.toString()); // MONDAY

        /*
         * compareTo()方法:比较两个枚举常量对象 == 比较编号
         *     public final int compareTo(E o) {
         *         Enum<?> other = (Enum<?>)o;
         *         Enum<E> self = this;
         *         if (self.getClass() != other.getClass() && // optimization
         *             self.getDeclaringClass() != other.getDeclaringClass())
         *             throw new ClassCastException();
         *         return self.ordinal - other.ordinal; // 比较编号
         *     }
         */
        System.out.println(m.compareTo(f)); // -4
    }
}

注解与元注解

注解

概念:注解是绑定到程序源代码元素的元数据,对运行代码的操作没有影响。

三个基本注解: @Deprecated @Override @SuppressWarnings

public class Deprecated_ {
    public static void  main(String[]args){
        A a = new A();
    }
}
/*
 * @Deprecated
 * 特征:有划线
 * 1.该注解修饰某个元素,表示该元素已经过时
 * 2.即不推荐使用,但可以用
 *
 * @Override
 * 限定于某个方法,是重写父类的方法
 *
 * @SuppressWarnings
 * 抑制编译器警告 
 * 格式:@SuppressWarning({"","",""...});
 * 例子:@SuppressWarning({"all"}); 抑制所有警告
 * 通常放在方法和类上
 * @SuppressWarning 中的属性介绍以及属性说明
 * all,抑制所有警告
 * boxing,抑制与封装/拆装作业相关的警告
 * cast,抑制与强制转型作业相关的警告
 * dep-ann,抑制与淘汰注释相关的警告
 * deprecation,抑制与淘汰的相关警告
 * fallthrough,抑制与switch陈述式中遗漏break相关的警告
 * finally,抑制与未传回finally区块相关的警告
 * hiding,抑制与隐藏变数的区域变数相关的警告
 * incomplete-switch,抑制与switch陈述式(enum case)中遗漏项目相关的警告
 * javadoc,抑制与javadoc相关的警告
 * nls,抑制与非nls字串文字相关的警告
 * null,抑制与空值分析相关的警告
 * rawtypes,抑制与使用raw类型相关的警告
 * resource,抑制与使用Closeable类型的资源相关的警告
 * restriction,抑制与使用不建议或禁止参照相关的警告
 * serial,抑制与可序列化的类别遗漏serialVersionUID栏位相关的警告
 * static-access,抑制与静态存取不正确相关的警告
 * static-method,抑制与可能宣告为static的方法相关的警告
 * super,抑制与置换方法相关但不含super呼叫的警告
 * synthetic-access,抑制与内部类别的存取未最佳化相关的警告
 * sync-override,抑制因为置换同步方法而遗漏同步化的警告
 * unchecked,抑制与未检查的作业相关的警告
 * unqualified-field-access,抑制与栏位存取不合格相关的警告
 * unused,抑制与未用的程式码及停用的程式码相关的警告
 */
@Deprecated
class A{
    public void fly(){}
}
class B extends A{
    @Override
    public void fly(){}
}
元注解(了解)

概念: 用来修饰注解(如@Override)

Retention 指定注解的作用范围,三种SOURCE,CLASS,RUNTIME

  • RetentionPolicy.SOURCE 该注解经过编译后被丢弃
  • RetentionPolicy.CLASS 该注解在解释之后被丢弃
  • RetentionPolicy.RUNTIME 该注解会被保存到JVM虚拟机中

Target 指定注解可以在哪些地方使用

Documented 指定该注解是否会在javadoc体现

Inherited 子类会继承父类的注解

// @Override的源码案例分析
@Target(ElementType.METHOD) // 方法中使用
@Retention(RetentionPolicy.SOURCE)  // 该注解主要是用于编译,所以在编译之后丢弃
public @interface Override { // @interface 不是接口,代表注解类(jdk5.0之后添加)
}

异常

基本概念

概念: 将程序执行中发生的不正常情况(语法错误和逻辑错误不是异常)

public class Exception01 {
    /*
     * 异常
     * 分两大点 Error & Exception
     * Error: 严重错误,程序奔溃 例如:java虚拟机无法解决的严重问题
     * Exception:偶然的外在因素导致的一般性问题,可以使用针对性代码进行处理
     * Exception 可以分为 运行时异常 和 编译时异常
     */
    public static void main(String[] args) {
        int num1 = 10,num2 = 0;
        /*
         * Exception in thread "main" java.lang.ArithmeticException: / by zero
         *     at com.Al_tair.exception_.Exception01.main(Exception01.java:14)
         */
        // int div = num1/num2; 异常

        try {
            int div = num1/num2;
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("~~~"); // 会继续运行
    }
}

异常的体系图

异常分为两大类:运行时异常,编译时异常

  • 运行时异常,编译器不要求强制处置的异常(一般是逻辑错误)
  • 编译时异常是编译器要求必须处置的异常

image-20220222105957339

运行时异常

运行时异常,编译器不要求强制处置的异常(一般是逻辑错误)

空指针异常

NullPointException :当应用程序在需要使用对象的地方使用null,抛出该异常

// 实例
class NullPointException{
    public static void main(String[] args) {
        String name = null;
        System.out.println(name.length()); // NullPointerException
    }
}
数字运算异常

ArithmeticException:当出现异常运算条件时,抛出异常

// 实例
class ArithmeticException {
    public static void main(String[] args) {
        int num1 = 10,num2 = 0;
        int div = num1/num2; // ArithmeticException
    }
}
数组下标越界

ArrayIndexOutOfBoundsException:用非法索引访问数组的异常

// 实例
class ArrayIndexOutOfBoundsException {
    public static void main(String[] args) {
        int[] array = new int[3];
        System.out.println(array[3]); //ArrayIndexOutOfBoundsException
    }
}
类型转换异常

ClassCastException:当试图将对象强制类型转换为不是该实例的子类,抛出该异常

// 实例
class ClassCastException {
    public static void main(String[] args) {
         A b = new B();
         C c = (C)b; // ClassCastException
    }
}
class A{}
class B extends A{}
class C extends A{}
数字格式不正确异常

NumberFormatException:当应用程序试图将字符串转换成一种数值类型,将抛出该异常

// 实例
class NumberFormatException {
    public static void main(String[] args) {
        String name = "";
        int num = Integer.parseInt(name); // NumberFormatException
    }
}

编译异常

编译时异常是编译器要求必须处置的异常

image-20220222153412877

异常处理

两种处理方式

  • try - catch - finally 程序员在代码中捕获异常,自行处理

  • throws 将异常抛出,交给调用者来处理,最顶级处理者JVM

    • 运行时异常:如果没有处理,默认throws的方式处理

    • 编译时异常:程序必须抛出异常或者处理异常,并且当子类重写父类的方法时,对抛出的异常的规定:子类重写的方法,所抛出的异常类型要么和父类抛出的异常一样,要么为父类抛出异常的子类型

      class Father{
          public void methos() throws RuntimeException{}
      }
      
      class Son extends Father{
          @Override
          public void methos() throws NullPointerException{
              super.methos();
          }
      }
      

try - catch - finally格式:

try{
    // 代码可能有异常
    // 如果没有出现异常,则执行try块中所有的语句,反之有出现异常,则不再执行try块中剩余的语句
}catch(){
    // 捕获到异常
    // 1.当异常发生时,才会执行catch内的代码
    // 2.系统将异常封装成Exception对象e,传递给catch
}finally{
    // 不管是否有异常,都会执行
}

// 注意细节
// 可以有多个catch语句进行捕获不同的异常,要求子类异常在父类异常之前,不然就没有子类异常存在的意义
// 可以进行try-finally配合使用,不进行其他异常捕获(包括throws),不管是否有异常都执行某些语句,然后程序退出

throws 抛出形式:

image-20220222155846503

习题一

当异常处理后需要返回数据,catch和finally中都有return语句,最终返回的数据是finally中的数据

image-20220222162034818

习题二

image-20220222161954872

自定义异常

继承Throwable的子类或者间接子类

运行时异常通常继承RuntimeException;编译时异常通常继承Exception

// 例子
class test{
    public static void main(String[] args) {
        int age = 208;
        /*
         * Exception in thread "main" com.Al_tair.exception_.AgeJudge: 不符合年龄
         * 	at com.Al_tair.exception_.test.main(Exception01.java:71)
         */
        if(!(age >= 0 && age <= 140)){
            throw new AgeJudge("不符合年龄");
        }
    }
}
class AgeJudge extends RuntimeException{
    public AgeJudge(String message) {
        super(message);
    }
}

throws VS throw

意义位置后面的语句
throws异常处理的一种方式方法声明处异常类型
throw手动生成异常对象的关键字方法体中异常对象

相关面试题

1.请说明JAVA语言如何进行异常处理,关键字:throws,throw,try,catch,finally分别代表什么意义?在try块中可以抛出异常吗?

Java 通过面向对象的方法进行异常处理,把各种不同的异常进行分类,并提供了良好的接口。在Java中,每个异常都是一个对象,它是Throwable类或其它子类的实例。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并进行处理。

Java的异常处理是通过5个关键词来实现的:try、catch、throw、throws和finally。一般情况下是用try来执行一段程序,如果出现异常,系统会抛出(throws)一个异常,这时候你可以通过它的类型来捕捉(catch)它,或最后(finally)由缺省处理器来处理。用try来指定一块预防所有”异常”的程序。紧跟在try程序后面,应包含一个catch子句来指定你想要捕捉的”异常”的类型。throw语句用来明确地抛出一个”异常”。throws用来标明一个成员函数可能抛出的各种”异常”。Finally为确保一段代码不管发生什么”异常”都被执行一段代码。可以在一个成员函数调用的外面写一个try语句,在这个成员函数内部写另一个try语句保护其他代码。每当遇到一个try语句,”异常“的框架就放到堆栈上面,直到所有的try语句都完成。如果下一级的try语句没有对某种”异常”进行处理,堆栈就会展开,直到遇到有处理这种”异常”的try语句。

常用类

包装类(Wrapper)

包装类的分类
包装类基本数据类型直接父类
booleanBooleanObject
charCharacterObject
byteByteNumber
shortShortNumber
intIntNumber
longLongNumber
floatFloatNumber
doubleDoubleNumber

Boolean

image-20211026161841862

Character

image-20211026162014901

Number父类下的直接子类

image-20211026165801659

装箱 & 拆箱
  • 自动拆装箱
  • 手动拆装箱
public class Wrapper01 {
    public static void main(String[] args) {
        // jdk5以前手动装箱&手动拆箱;jdk5之后可以自动拆装箱
        // 以Character为例
        char name = 'n';
        // 手动装箱
        Character ch1 = new Character(name); // 不推荐
        Character ch2 = Character.valueOf(name);
        // 手动拆箱
        char name2 = Character.valueOf(ch2); // 本质就是使用charValue方法
        char name3 = ch1.charValue();

        // 自动装箱
        Character ch3 = name; // 本质使用的就是valueOf方法
        // 自动拆箱
        char CH4 = ch3; // 本质就是使用charValue方法
    }
}

接下来我对于自动拆装箱的底层进行追踪结果

首先打四个断点,分别探索这四个断点的跳转

image-20211027111537252

以下是依次跳转的函数

image-20211027111819114

image-20211027111852782

image-20211027111925147

image-20211027111951932

总结

  1. 手动拆装箱和自动拆装箱底层没有本质区别
  2. 包装类转 <=>基本数据类型
    • 基本数据类型 --> 包装类 装箱 本质:valueOf函数
    • 包装类 --> 基本数据类型 拆箱 本质:charValue函数

习题

// 如下输出结果是什么
习题1
Object obj = true? new Integer(1):new Double(2.0); // 三元运算符是一个整体
System.out.println(obj); // 1.0
习题2
Object obj1;
if(true){
    obj1 = new Integer(1);
}else{
    obj1 = new Double(2.0);
}
System.out.println(obj); // 1
包装类 <=> String类
Wrapper Vs String
public class WrapperVsString {
    public static void main(String[]args){
        // String类 转换成 包装类
        String age = "120"; 
        Integer age2 = Integer.valueOf(age);  // 方式一:valueOf函数 本质上就是parseInt()方法
        Integer a2 = Integer.parseInt(age); // 方式二:parseInt函数
        Integer age3 = new Integer(age);  //不推荐,本质就是parseInt()方法

        // 包装类 转换成 String类
        Integer height = 180; // 自动装箱
        String h = String.valueOf(height); // 方式一:valueOf函数 本质就是调用toString()方法
        String h2 = height + "";  // 方式二: 类型转换 Integer + ""
        String h3 = height.toString(); // 方式三: toString()函数

        /*
         *   String.valueOf()源码
         *   public static String valueOf(Object obj) {
         *       return (obj == null) ? "null" : obj.toString();
         *   }
         * 
         *   Integer.valueOf()源码
         *   public static Integer valueOf(String s) throws NumberFormatException {
         *        return Integer.valueOf(parseInt(s, 10)); // 10指的是传入的数字是十进制数
         *   }
         *
         *   new Integer()源码
         *   @Deprecated(since="9")
         *   public Integer(String s) throws NumberFormatException {
         *          this.value = parseInt(s, 10);
         *   }
         */
    }
}
Wrapper类的常用方法

Integer 官方文档

以Integer包装类为例

Integer

包装类的相关面试题

public class Wrapper02 {
    public static void main(String[] args) {
        /*
         * 源码:IntegerCache.low -128   IntegerCache.high 127
         *     public static Integer valueOf(int i) {
         *         if (i >= IntegerCache.low && i <= IntegerCache.high)
         *             return IntegerCache.cache[i + (-IntegerCache.low)];
         *         return new Integer(i);
         *     }
         * 如果valueOf(value) value > -128 && value < 127 则 返回 IntegerCache.cache[i + (-IntegerCache.low)]
         * 否则 返回新对象Integer
         */
        System.out.println(new Integer(1) == new Integer(1));  // false
        Integer a = 1;
        Integer b = 1;
        System.out.println(a==b); // true
        Integer m = 128;
        Integer n = 128;
        System.out.println(m==n); // false
        Integer x = 128;
        int y = 128;
        System.out.println(x==y); // true
        
    }
}

String类

String类的概述
public static void main(String[] args) {
       /**
         * String
         * 概念:是一组字符序列 本质上是char[] value 字符数组实现
         * "Al_tair"被称为字符常量 用双引号括起来的字符序列
         *  一个字符占用两个字节(每个字符不区分字母和汉字)
         * public final class String 说明String的final类,不能被其它类继承
         * private final byte[] value 用于存放字符串 value是用final修饰的类型,该数组不能指向新地址,但是能修改它的值
         */
    String name = "Al_tair";
}
接口和构造器

image-20211028164429590

String内存图
// 运行代码,内存图如下
class code{
    public static void main(String[] args){
        String a = "Al_tair";
        String b = new String("Al_tair");
    }
}

image-20220223163658951

内存图: 字符串 VS 字符数组

结合代码和内存图分析

class Text{
    String str = new String("lns");
    // final指的是char类型数据存储的地址不能改变,但是值是可以改变的
    final char[] ch = {'j','a','v','a'};
    public void change(String str,char[] ch){
        str = "zlr";
        ch[1] = 'c';
    }
    public static void main(String[] args) {
        Text text = new Text();
        text.change(text.str,text.ch);
        System.out.println(text.str.toString()+" and "+text.ch[1]); // lnsandc
    }
}

image-20220223162328369

String类的常用方法
  • equals 区别大小写,判断字符串的内容是否相同
  • equalsIgnoreCase 忽略大小写 来判断字符串的内容是否相同
  • length 获取字符串的个数,或者称为字符串长度
  • indexOf 获取字符在字符串中第一次出现的索引,索引从0开始,如果没有找到则返回-1
  • lastindexOf 获取字符在字符串中最后一次出现的索引,索引从0开始,如果没有找到则返回-1
  • substring 截取指定范围的字串
  • trim 去掉字符串前后的空格
  • charAt 获取某索引处的字符
  • compareTo 比较两个字符串的大小,如果前者大于等于后者,则返回自然数;反之后者大,则返回负数
  • intern 如果常量池中已经包含值相同的字符串,则返回常量池中的字符串引用地址,否则将String对象添加到常量池中,并返回String对象的引用
// equals()方法源码
public boolean equals(Object anObject) {
    if (this == anObject) { // 地址是否相同
        return true;
    }
    if (anObject instanceof String) { // 是否为String类或者String父类
        String aString = (String)anObject;
        if (!COMPACT_STRINGS || this.coder == aString.coder) {
            return StringLatin1.equals(value, aString.value);
        }
    }
    return false;
}
@HotSpotIntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
    if (value.length == other.length) {
        for (int i = 0; i < value.length; i++) {
            if (value[i] != other[i]) {
                return false;
            }
        }
        return true;
    }
    return false;
}

// 占位符的讲解 涉及方法format <=> c语言输出
// %s,%d,%.3f,%c
String name = "lns";
int age = 18;
double height = 185.35;
char gender = '男';

String Info = "姓名:%s\t年龄:%d\t身高:%.3f\t性别:%c";
String show = String.format(Info,name,age,height,gender);
System.out.println(show); // 姓名:lns	年龄:18	身高:185.350	性别:男

String 官方文档

String

相关习题
// 习题1
String a = "l";
String b = new String("l");
System.out.println(a.equals(b)); // true
System.out.println(a == b); // false
System.out.println(a == b.intern()); // true
System.out.println(b == b.intern()); // false

// 习题2
// 2.1创建了几个对象 答:2
String s = "hello";
s = "haha";

// 2.2 创建了几个对象 答:1  结论:编译器会做优化,判断常量池对象是否有引用指向
String str = "hello" + "haha";  // 等价于 String str = "hellohaha";

// 2.3 创建了几个对象 答:3  结论:字符串常量相加地址存放在常量池,字符串变量相加地址存放在String对象中
// sum 指向的是value[](String对象),再指向常量池中"HelloString"字符串
public static void main(String[]args){
    String m = "Hello";
    String n = "String";
    /*
     * 解读:
     * 1. 创建新对象 new StringBuilder();
     * 2. 通过append函数添加字符串 “Hello”
     * 3. 通过append函数添加字符串 “String”
     * 4. 返回new String("HelloString");
     */
    String sum = m + n;
}
// 分析sum 的指向和底层源码
// debug test
// first insert
public StringBuilder() {
    super(16);
}
//secong insert  str = "Hello"
public StringBuilder append(String str) {  
    super.append(str);
    return this;
}
// third insert str = "String"
public StringBuilder append(String str) {
    super.append(str);
    return this;
}
// last one
public String toString() {
    // Create a copy, don't share the array
    return isLatin1() ? StringLatin1.newString(value, 0, count): StringUTF16.newString(value, 0, count);
}

StringBuffer类

概念:代表可变的字符序列,可以对字符串内容进行增删,是一个容器

image-20220223203041672

构造方法
Constructor and Description
StringBuffer()构造一个没有字符的字符串缓冲区,初始容量为16个字符。
StringBuffer(CharSequence seq)构造一个包含与指定的相同字符的字符串缓冲区 CharSequence
StringBuffer(int capacity)构造一个没有字符的字符串缓冲区和指定的初始容量。
StringBuffer(String str)构造一个初始化为指定字符串内容的字符串缓冲区。
/*
 * Constructs a string buffer with no characters in it and an
 * initial capacity of 16 characters.
 * StringBuffer()构造器
 */
@HotSpotIntrinsicCandidate
public StringBuffer() {
    super(16); // 初始容量为16个字符 存储在父类的value数组中
}
String类 <=> StringBuffer类

String类和StringBuffer类的区别

  • String保存的是字符串常量,里面的值不能更改,每次值的更新实际上就是更改地址,效率低
  • Stringbuffer保存的是字符串变量,里面的值是可以改变的,不需要每次都更改地址,效率高

String类和StringBuffer类的相互转换

public static void main(String[] args) {
    // String和StringBuffer的相互转换
    // String => StringBuffer
    String str = "lns";
    StringBuffer stringBuffer = new StringBuffer(str); // 方式一: 使用StringBuffer构造器
    StringBuffer append = new StringBuffer().append(str); // 方式二: 使用的是append方法

    // StringBuffer => String
    StringBuffer sbr = new StringBuffer("zlr");
    String s = sbr.toString(); // 方式一: 使用toString方法
    String s1 = new String(sbr); // 使用String构造器 
}
常用方法
public static void main(String[] args) {
    // 常用方法
    // append 增
    StringBuffer stringBuffer = new StringBuffer("");
    stringBuffer.append("lns"); // lns
    /*
     *  append源码
     *  不管传入什么数据类型,返回StringBuffer类型
     *  public synchronized StringBuffer append(String str) {
     *      toStringCache = null;
     *      super.append(str);
     *      return this;
     *  }
     */

    // delete 删除
    // 删除索引范围 [start,end)
    stringBuffer.delete(0,1); // 删除第一个字符 ns

    // replace 替换
    // 替换范围[start,end)
    stringBuffer.replace(0, 1,"ln"); // lns

    // indexOf 查找
    // 查找第一次在字符串中出现的索引,如果查找到会返回你查找的字符串首个字母索引,如果找不到返回-1
    stringBuffer.indexOf("ns"); // 1

    // length 长度
    System.out.println(stringBuffer.length()); // 3
}
相关习题
// 习题1
String str = null;
StringBuffer sb = new StringBuffer();
sb.append(str);
System.out.println(sb); // null
System.out.println(sb.length()); // 4  
/*
 *  // 底层分析
 *  // StingBuffer类
 *  public synchronized StringBuffer append(String str) {
 *      toStringCache = null;
 *      super.append(str); // 跳转到父类
 *      return this;
 *  }
 *  // AbstractStringBuilder抽象类
 *  public AbstractStringBuilder append(String str) {
 *      if (str == null) {
 *          return appendNull(); // 跳转到该方法
 *      }
 *      int len = str.length();
 *      ensureCapacityInternal(count + len);
 *      putStringAt(count, str);
 *      count += len;
 *      return this;
 *  }
 *  // appendNull方法
 *  private AbstractStringBuilder appendNull() {
 *      ensureCapacityInternal(count + 4);
 *      int count = this.count;
 *      byte[] val = this.value;
 *      if (isLatin1()) {
 *          val[count++] = 'n';
 *          val[count++] = 'u';
 *          val[count++] = 'l';
 *          val[count++] = 'l';
 *      } else {
 *          count = StringUTF16.putCharsAt(val, count, 'n', 'u', 'l', 'l');
 *      }
 *      this.count = count;
 *      return this;
 *  }
 */
 StringBuffer sb = new StringBuffer(str); // 抛出空指针异常 NullPointerException
 /*
 * AbstractStringBuilder(String str) {
 *    int length = str.length(); // str为null 
 *    int capacity = (length < Integer.MAX_VALUE - 16)
 *           ? length + 16 : Integer.MAX_VALUE;
 *    final byte initCoder = str.coder();
 *    coder = initCoder;
 *    value = (initCoder == LATIN1)
 *           ? new byte[capacity] : StringUTF16.newBytesFor(capacity);
 *    append(str);
 * }
 */

StringBuilder类

概念:一个可变的字符序列。 线程不安全。 此类设计用作简易替换为StringBuffer在正在使用由单个线程字符串缓冲区的地方。 在可以的情况下,建议使用这个类别优先于StringBuffer ,因为它在大多数实现中将更快。

大部分与 StringBuffer类似

image-20220224105411449

特殊点:没有做互斥处理,因此在单线程下使用

// 源码剖析 区别在于关键字 synchronized 保证线程安全
// StringBuffer 的append方法
@Override
@HotSpotIntrinsicCandidate
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

// StringBuilder 的append方法
@Override
@HotSpotIntrinsicCandidate
public StringBuilder append(String str) {
    super.append(str);
    return this;
}

String,StringBuffer,StringBuilder的区别

  • String:不可变字符序列,效率低,但是因为存在常量池所以复用率高
  • StringBuffer:可变字符序列,效率较高(增删),线程安全 、
  • StringBuilder:可变字符序列,效率最高,线程不安全

使用原则

  • 如果字符串存在大量的修改操作,一般使用StringBuffer或者StringBuider
  • 如果字符串存在大量的修改操作,并在单线程的情况,使用StringBuilder
  • 如果字符串存在大量的修改操作,并在多线程的情况,使用StringBuffer
  • 如果字符串很少修改,被多个对象引用,使用String 比如:配置信息等

Math类

概念:Math类包含执行基本数学运算的方法

常用方法
public static void main(String[] args) {
    // Math类中大部分是静态方法,可以直接通过类名.方法名访问
    // abs 绝对值
    int abs = Math.abs(-10);
    System.out.println(abs); // 10

    // pow 求幂
    double pow = Math.pow(2,4);
    System.out.println(pow); // 16.0

    // ceil 向上取整,返回>=该参数的最小整数(整数会转换成double型)
    double ceil = Math.ceil(-3.002);
    System.out.println(ceil); // -3.0

    // floor 向下取整,返回<=该参数的最大整数(整数会转换成double型)
    double floor = Math.floor(3.2);
    System.out.println(floor); // 3.0

    // round 四舍五入 <=> Math.floor(参数+0.5)
    double round = Math.round(3.24);
    System.out.println(round); // 3.0

    // sqrt 求开平方
    double sqrt = Math.sqrt(4);
    System.out.println(sqrt); // 2.0

    // random 随机数 [0,1)
    int random = (int)(Math.random()*50+50);
    System.out.println(random); // 整数范围 [50,100)
}

Arrays类

概念:该类包含用于操作数组的各种方法(如排序和搜索),大部分方法也是静态方法

常用方法
toString方法

作用:输出数组

Integer[] array = {3,5,6,47,8};
// toString 输出数组
System.out.println(Arrays.toString(array)); // [3, 5, 6, 47, 8]
/*
 * // toString方法源码
 * public static String toString(int[] a) {
 *   if (a == null)
 *      return "null";
 *   int iMax = a.length - 1;
 *   if (iMax == -1)
 *      return "[]";
 *
 *   StringBuilder b = new StringBuilder();
 *   b.append('[');
 *   for (int i = 0; ; i++) {
 *      b.append(a[i]);
 *      if (i == iMax)
 *         return b.append(']').toString();
 *       b.append(", ");
 *   }
 * }
 */       
sort方法

作用:排序数组默认从小到大

// sort重载,可以通过传入一个接口Comparator实现定制排序
Integer[] array = {3,5,6,47,8};
Arrays.sort(array);
System.out.println(Arrays.toString(array)); // [3, 5, 6, 8, 47]
Arrays.sort(array,new Comparator(){
    @Override
    public int compare(Object o1, Object o2) {
        Integer i1 = (Integer)o1;
        Integer i2 = (Integer)o2;
        return i2 - i1; // 决定是升序还是降序
    }
});
System.out.println(Arrays.toString(array)); // [47, 8, 6, 5, 3]
/**
 * MySort的冒泡实现
 * public class MySort {
 *     public static void main(String[] args) {
 *         int[] arr = {6,4,5,6,845,4,51};
 *         bubble(arr, new Comparator() {
 *             @Override
 *             public int compare(Object o1, Object o2) {
 *                 int i1 = (Integer)o1;
 *                 int i2 = (Integer)o2;
 *                 return i1 - i2;
 *             }
 *         });
 *         System.out.println(Arrays.toString(arr));
 *     }
 *
 *     public static void bubble(int[] arr, Comparator c){
 *         int temp = 0;
 *         for (int i = 0; i < arr.length - 1; i++) {
 *             for (int j = 0; j < arr.length - 1 - i; j++) {
 *                 if(c.compare(arr[j],arr[j+1]) >= 0){
 *                     temp = arr[j];
 *                     arr[j] = arr[j+1];
 *                     arr[j+1] = temp;
 *                 }
 *             }
 *         }
 *     }
 * }
 */
binarySearch方法

作用:通过二分搜索法进行查找,要求必须升序,如果数组中不存在,则返回 -(应该在的索引位置 + 1)

Integer[] array = {3,5,6,47,8};
Arrays.sort(array); // [3, 5, 6, 8, 47]
int index = Arrays.binarySearch(array,9); 
System.out.println(index); // -5 应该在索引4位置(8和471之间),返回-(4+1)
/**
 * binarySearch 源码
 * private static int binarySearch0(Object[] a, int fromIndex, int toIndex, Object key) {
 *   int low = fromIndex;
 *   int high = toIndex - 1;
 *
 *   while (low <= high) {
 *      int mid = (low + high) >>> 1;
 *      @SuppressWarnings("rawtypes") // 抑制警告
 *      Comparable midVal = (Comparable)a[mid];
 *      @SuppressWarnings("unchecked")
 *      int cmp = midVal.compareTo(key);
 *
 *       if (cmp < 0)
 *          low = mid + 1;
 *       else if (cmp > 0)
 *          high = mid - 1;
 *       else
 *          return mid; // key found
 *    }
 *    return -(low + 1);  // key not found.
 *  }
 */
其他方法
// copeOf 数组的赋值 如果赋值的长度大于原数组的长度,则多余的数据用null填入
Integer[] integers = Arrays.copyOf(array, array.length-1);
System.out.println(Arrays.toString(integers)); // [3, 5, 6, 8]

// fill 数组的填充 替换数组中的所有数据
int[] fillNum = {2,45,78,85,15};
Arrays.fill(fillNum,2);
System.out.println(Arrays.toString(fillNum)); // [2, 2, 2, 2, 2]

// equals 比较两个数组元素内容是否相同
int[] equalsNum = {2,45,78,85,15};
int[] equalsNum2 = {2,45,78,85,15};
System.out.println(Arrays.equals(equalsNum,equalsNum2)); // true

System类

概念:System类包含几个有用的类字段和方法。 它不能被实例化。

常用方法
public static void main(String[] args) {
    // gc 方法 垃圾回收器
    new System01();
    System.gc(); // 我已经被销毁了...

    // currentTimeMillis 方法 在1970年1月1日UTC之间的当前时间和午夜之间的差异,以毫秒为单位。
    System.out.println(System.currentTimeMillis()); // 1645776480314

    // arraycopy 方法 复制数组
    int[] src = {1,2,3};
    int[] desc = {0,0,0};
    /*
     * 从左到右的五个参数描述
     *  src      the source array. 被复制内容的数组
     *  srcPos   starting position in the source array. 源数组索引位置(从哪个位置开始拷贝)
     *  dest     the destination array. 复制内容得到的数组
     *  destPos  starting position in the destination data. 目标数组的索引位置
     *  length   the number of array elements to be copied. 拷贝的数组长度
     */
    System.arraycopy(src,0,desc,0,3);
    System.out.println(Arrays.toString(desc)); //[1, 2, 3]
    System.out.println(src == desc); // false

    // exit 方法 退出
    System.out.println("程序开始");
    /*
     * status说明例子
     * 在一个if-else判断中,如果我们程式是按照我们预想的执行,
     * 到最后我们需要停止程式,那么我们使用System.exit(0),
     * 而System.exit(1)一般放在catch块中,当捕获到异常,需要停止程式,
     * 我们使用System.exit(1)。这个status=1是用来表示这个程式是非正常退出。
     */
    System.exit(0); // System.exit(0)是正常退出程序,而System.exit(1)或者说非0表示非正常退出程序
    System.out.println("程序结束"); // 不执行
}
@Override
protected void finalize(){
    System.out.println("我已经被销毁了...");
}

BigIneger和BigDecimal类

概念:BigIneger 适合保存比较大的整型数据;BigDecimal 适合保存精度更高的浮点型数据

// BigIneger 适合保存比较大的整型数据  long数据类型无法存储
BigInteger bigInteger = new BigInteger("998456349564561256465489");
System.out.println(bigInteger); // 998456349564561256465489
// + - * / 运算 => 方法实现 add  subtract multiply divide
bigInteger = bigInteger.add(new BigInteger("1"));
System.out.println(bigInteger); // 998456349564561256465490
bigInteger = bigInteger.divide(new BigInteger("2"));
System.out.println(bigInteger); // 499228174782280628232745
bigInteger = bigInteger.subtract(new BigInteger("2"));
System.out.println(bigInteger); // 499228174782280628232743
bigInteger = bigInteger.multiply(new BigInteger("2"));
System.out.println(bigInteger); // 998456349564561256465486

// BigDecimal 适合保存精度更高的浮点数  double数据类型无法存储
BigDecimal bigDecimal = new BigDecimal("9980.2561295645485648548485646541");
System.out.println(bigDecimal); // 9980.2561295645485648548485646541
// + - * / 运算 => 方法实现 add  subtract multiply divide
bigDecimal = bigDecimal.add(new BigDecimal("1"));
System.out.println(bigDecimal); // 9981.2561295645485648548485646541
bigDecimal = bigDecimal.divide(new BigDecimal("2")); // 如果除不尽则返回算术异常
System.out.println(bigDecimal); // 4990.62806478227428242742428232705
bigDecimal = bigDecimal.subtract(new BigDecimal("2"));
System.out.println(bigDecimal); // 4988.62806478227428242742428232705
bigDecimal = bigDecimal.multiply(new BigDecimal("2"));
System.out.println(bigDecimal); // 9977.25612956454856485484856465410
// 解决小数除法异常问题:指定精度(JDK9以后不建议使用)
bigDecimal = bigDecimal.divide(new BigDecimal("2.3326"),BigDecimal.ROUND_CEILING);
System.out.println(bigDecimal); // 4277.31121047952866537548167909376

日期类

第一代日期类

Date:精确到毫秒,代表瞬间

SimpleDateFormat:格式和解析日期类(日期 <=> 文本)

public static void main(String[] args) throws ParseException {
// Date 日期类
Date date = new Date(); // 当前日期
System.out.println(date); // Fri Feb 25 16:58:51 CST 2022
Date date2 = new Date(4564956); // 输入距离1970年1月1日的毫秒数
System.out.println(date2); // Thu Jan 01 09:16:04 CST 1970

// SimpleDateFormat 格式和解析日期类 按照自己的格式的日期    年  月  日    时 分 秒 星期 (规定如下图)
SimpleDateFormat sdf = new SimpleDateFormat("YYYY年MM月DD日 hh:mm:ss E");
System.out.println(sdf.format(date)); // 2022年02月56日 05:07:32 周五

String dateStr = "2021年02月56日 05:07:32 周一";
System.out.println(sdf.format(sdf.parse(dateStr))); // 2021年12月363日 05:07:32 周一 会存在编译异常
}

SimpleDateFormat的规定格式

image-20220225170623223

第二代日期类

Calendar类(日历) 是一个抽象类

// 抽象类 可以通过getInstance方法获取实例
Calendar calendar = Calendar.getInstance();
System.out.println(calendar); 
System.out.println("年:"+calendar.get(calendar.YEAR)); // 年:2022
System.out.println("月:"+calendar.get(calendar.MONTH)+1); // 月:2 源码:JANUARY} which is 0
System.out.println("日:"+calendar.get(calendar.DAY_OF_MONTH)); // 日:25
System.out.println("小时:"+calendar.get(calendar.HOUR)); // 小时:8
System.out.println("分钟:"+calendar.get(calendar.MINUTE)); // 分钟:11
System.out.println("秒:"+calendar.get(calendar.SECOND)); // 秒:46
第三代日期类 (JDK8)

LocalDate 日期:年月日

LocalTime 时间:时分秒

LocalDateTime:年月日 时分秒

LocalDateTime localDateTime = LocalDateTime.now();
LocalTime localTime = LocalTime.now();
LocalDate localDate = LocalDate.now();
// localDateTime: 2022-02-25T20:30:19.250574 LocalTime: 20:30:19.250574 LocalDate: 2022-02-25
System.out.println("localDateTime: "+localDateTime+" LocalTime: "+
                   localTime+" LocalDate: "+localDate);

System.out.println("年: "+localDateTime.getYear()); // 年: 2022
System.out.println("月: "+localDateTime.getMonth()); // 月: FEBRUARY
System.out.println("日: "+localDateTime.getDayOfMonth()); // 日: 25
System.out.println("时: "+localDateTime.getHour()); // 时: 20
System.out.println("分: "+localDateTime.getMinute()); // 分: 33
System.out.println("秒: "+localDateTime.getSecond()); // 秒: 45

DateTimeFormatter格式日期类

//  DateTimeFormatter 格式日期类
LocalDateTime localDateTime = LocalDateTime.now();
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("YYYY年MM月DD日 hh:mm:ss E");
System.out.println(dateTimeFormatter.format(localDateTime)); // 2022年02月56日 08:39:43 周五
// 所有字母“A”至“Z”和“a”至“z”保留为图案字母。 定义了以下图案字母: 
Symbol  Meaning                     Presentation      Examples
  ------  -------                     ------------      -------
   G       era                         text              AD; Anno Domini; A
   u       year                        year              2004; 04
   y       year-of-era                 year              2004; 04
   D       day-of-year                 number            189
   M/L     month-of-year               number/text       7; 07; Jul; July; J
   d       day-of-month                number            10

   Q/q     quarter-of-year             number/text       3; 03; Q3; 3rd quarter
   Y       week-based-year             year              1996; 96
   w       week-of-week-based-year     number            27
   W       week-of-month               number            4
   E       day-of-week                 text              Tue; Tuesday; T
   e/c     localized day-of-week       number/text       2; 02; Tue; Tuesday; T
   F       week-of-month               number            3

   a       am-pm-of-day                text              PM
   h       clock-hour-of-am-pm (1-12)  number            12
   K       hour-of-am-pm (0-11)        number            0
   k       clock-hour-of-am-pm (1-24)  number            0

   H       hour-of-day (0-23)          number            0
   m       minute-of-hour              number            30
   s       second-of-minute            number            55
   S       fraction-of-second          fraction          978
   A       milli-of-day                number            1234
   n       nano-of-second              number            987654321
   N       nano-of-day                 number            1234000000

   V       time-zone ID                zone-id           America/Los_Angeles; Z; -08:30
   z       time-zone name              zone-name         Pacific Standard Time; PST
   O       localized zone-offset       offset-O          GMT+8; GMT+08:00; UTC-08:00;
   X       zone-offset 'Z' for zero    offset-X          Z; -08; -0830; -08:30; -083015; -08:30:15;
   x       zone-offset                 offset-x          +0000; -08; -0830; -08:30; -083015; -08:30:15;
   Z       zone-offset                 offset-Z          +0000; -0800; -08:00;

   p       pad next                    pad modifier      1

   '       escape for text             delimiter
   ''      single quote                literal           '
   [       optional section start
   ]       optional section end
   #       reserved for future use
   {       reserved for future use
   }       reserved for future use 

Instant 时间戳

// Instant -> Date
Instant instant = Instant.now();
System.out.println(instant); // 2022-02-25T14:48:47.557358800Z
java.util.Date from = Date.from(instant);
System.out.println(from); // Fri Feb 25 22:48:47 CST 2022

// Date -> Instant
Instant instant1 = from.toInstant();
System.out.println(instant1); // 2022-02-25T14:55:27.377Z

相关面试题

1.String类有哪些方法?

String类是Java最常用的API,它包含了大量处理字符串的方法,比较常用的有:

  • char charAt(int index):返回指定索引处的字符;
  • String substring(int beginIndex, int endIndex):从此字符串中截取出一部分子字符串;
  • String[] split(String regex):以指定的规则将此字符串分割成数组;
  • String trim():删除字符串前导和后置的空格;
  • int indexOf(String str):返回子串在此字符串首次出现的索引;
  • int lastIndexOf(String str):返回子串在此字符串最后出现的索引;
  • boolean startsWith(String prefix):判断此字符串是否以指定的前缀开头;
  • boolean endsWith(String suffix):判断此字符串是否以指定的后缀结尾;
  • String toUpperCase():将此字符串中所有的字符大写;
  • String toLowerCase():将此字符串中所有的字符小写;
  • String replaceFirst(String regex, String replacement):用指定字符串替换第一个匹配的子串;
  • String replaceAll(String regex, String replacement):用指定字符串替换所有的匹配的子串。

2.String可以被继承吗?

String类由final修饰,所以不能被继承。

扩展阅读

在Java中,String类被设计为不可变类,主要表现在它保存字符串的成员变量是final的。

  • Java 9之前字符串采用char[]数组来保存字符,即 private final char[] value;
  • Java 9做了改进,采用byte[]数组来保存字符,即 private final byte[] value;

之所以要把String类设计为不可变类,主要是出于安全和性能的考虑,可归纳为如下4点。

  • 由于字符串无论在任何 Java 系统中都广泛使用,会用来存储敏感信息,如账号,密码,网络路径,文件处理等场景里,保证字符串 String 类的安全性就尤为重要了,如果字符串是可变的,容易被篡改,那我们就无法保证使用字符串进行操作时,它是安全的,很有可能出现 SQL 注入,访问危险文件等操作。
  • 在多线程中,只有不变的对象和值是线程安全的,可以在多个线程中共享数据。由于 String 天然的不可变,当一个线程”修改“了字符串的值,只会产生一个新的字符串对象,不会对其他线程的访问产生副作用,访问的都是同样的字符串数据,不需要任何同步操作。
  • 字符串作为基础的数据结构,大量地应用在一些集合容器之中,尤其是一些散列集合,在散列集合中,存放元素都要根据对象的 hashCode() 方法来确定元素的位置。由于字符串 hashcode 属性不会变更,保证了唯一性,使得类似 HashMap,HashSet 等容器才能实现相应的缓存功能。由于 String 的不可变,避免重复计算 hashcode,只要使用缓存的 hashcode 即可,这样一来大大提高了在散列集合中使用 String 对象的性能。
  • 当字符串不可变时,字符串常量池才有意义。字符串常量池的出现,可以减少创建相同字面量的字符串,让不同的引用指向池中同一个字符串,为运行时节约很多的堆内存。若字符串可变,字符串常量池失去意义,基于常量池的 String.intern() 方法也失效,每次创建新的字符串将在堆内开辟出新的空间,占据更多的内存。

因为要保证String类的不可变,那么将这个类定义为final的就很容易理解了。如果没有final修饰,那么就会存在String的子类,这些子类可以重写String类的方法,强行改变字符串的值,这便违背了String类设计的初衷。

3.说一说String和StringBuffer有什么区别

String类是不可变类,即一旦一个String对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁。

StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象。

4.说一说StringBuffer和StringBuilder有什么区别

tringBuffer、StringBuilder都代表可变的字符串对象,它们有共同的父类 AbstractStringBuilder,并且两个类的构造方法和成员方法也基本相同。不同的是,StringBuffer是线程安全的,而StringBuilder是非线程安全的,所以StringBuilder性能略高。一般情况下,要创建一个内容可变的字符串,建议优先考虑StringBuilder类。

5.使用字符串时,new和""推荐使用哪种方式?

先看看 “hello” 和 new String(“hello”) 的区别:

  • 当Java程序直接使用 “hello” 的字符串直接量时,JVM将会使用常量池来管理这个字符串;
  • 当使用 new String(“hello”) 时,JVM会先使用常量池来管理 “hello” 直接量,再调用String类的构造器来创建一个新的String对象,新创建的String对象被保存在堆内存中。

显然,采用new的方式会多创建一个对象出来,会占用更多的内存,所以一般建议使用直接量的方式创建字符串。

6.两个字符串相加的底层是如何实现的?

如果拼接的都是字符串直接量,则在编译时编译器会将其直接优化为一个完整的字符串,和你直接写一个完整的字符串是一样的。

如果拼接的字符串中包含变量,则在编译时编译器采用StringBuilder对其进行优化,即自动创建StringBuilder实例并调用其append()方法,将这些字符串拼接在一起。

7.遇到过异常吗,如何处理?

在Java中,可以按照如下三个步骤处理异常:

  1. 捕获异常

    将业务代码包裹在try块内部,当业务代码中发生任何异常时,系统都会为此异常创建一个异常对象。创建异常对象之后,JVM会在try块之后寻找可以处理它的catch块,并将异常对象交给这个catch块处理。

  2. 处理异常

    在catch块中处理异常时,应该先记录日志,便于以后追溯这个异常。然后根据异常的类型、结合当前的业务情况,进行相应的处理。比如,给变量赋予一个默认值、直接返回空值、向外抛出一个新的业务异常交给调用者处理,等等。

  3. 回收资源

    如果业务代码打开了某个资源,比如数据库连接、网络连接、磁盘文件等,则需要在这段业务代码执行完毕后关闭这项资源。并且,无论是否发生异常,都要尝试关闭这项资源。将关闭资源的代码写在finally块内,可以满足这种需求,即无论是否发生异常,finally块内的代码总会被执行。

8.请介绍Java的异常接口

Throwable是异常的顶层父类,代表所有的非正常情况。它有两个直接子类,分别是Error、Exception。

Error是错误,一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断。通常应用程序无法处理这些错误,因此应用程序不应该试图使用catch块来捕获Error对象。在定义方法时,也无须在其throws子句中声明该方法可能抛出Error及其任何子类。

Exception是异常,它被分为两大类,分别是Checked异常和Runtime异常。所有的RuntimeException类及其子类的实例被称为Runtime异常;不是RuntimeException类及其子类的异常实例则被称为Checked异常。Java认为Checked异常都是可以被处理(修复)的异常,所以Java程序必须显式处理Checked异常。如果程序没有处理Checked异常,该程序在编译时就会发生错误,无法通过编译。Runtime异常则更加灵活,Runtime异常无须显式声明抛出,如果程序需要捕获Runtime异常,也可以使用try…catch块来实现。

9.int和Integer有什么区别,二者在做==运算时会得到什么结果?

int是基本数据类型,Integer是int的包装类。二者在做==运算时,Integer会自动拆箱为int类型,然后再进行比较。届时,如果两个int值相等则返回true,否则就返回false。

10.说一说自动装箱、自动拆箱的应用场景

自动装箱、自动拆箱是JDK1.5提供的功能。

自动装箱:可以把一个基本类型的数据直接赋值给对应的包装类型;

自动拆箱:可以把一个包装类型的对象直接赋值给对应的基本类型;

通过自动装箱、自动拆箱功能,可以大大简化基本类型变量和包装类对象之间的转换过程。比如,某个方法的参数类型为包装类型,调用时我们所持有的数据却是基本类型的值,则可以不做任何特殊的处理,直接将这个基本类型的值传入给方法即可。

11.为啥要有包装类?

Java语言是面向对象的语言,其设计理念是“一切皆对象”。但8种基本数据类型却出现了例外,它们不具备对象的特性。正是为了解决这个问题,Java为每个基本数据类型都定义了一个对应的引用类型,这就是包装类。

集合类

集合框架体系

Collection接口框架

image-20220226113621627

补充框架图

image-20220303102523695

Map接口框架

image-20220226114159934

补充框架图

image-20220303102607310

Collection接口

Collection 实现 Iterable接口 : public interface Collection extends Iterable

遍历方式
遍历方式-迭代器

image-20220226131905068

Iterator接口 又称为 迭代器,主要用于遍历Collcection集合中的元素

所有实现了Collection接口的集合类都有一个Iterator()方法,用来返回一个迭代器

迭代器方法

变量和类型方法描述
default voidforEachRemaining(Consumer<? super E> action)对每个剩余元素执行给定操作,直到处理完所有元素或操作引发异常。
booleanhasNext()如果迭代具有更多元素,则返回 true
Enext()返回迭代中的下一个元素。
default voidremove()从底层集合中移除此迭代器返回的最后一个元素(可选操作)。
// Iterator方法的使用实例
public class Iterator01 {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(new Book("三国演义",10));
        list.add(new Book("水浒传",20));
        list.add(new Book("西游记",15));

        Iterator iterator = list.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }
        // 退出循环之后,迭代器指向最后一个元素
        // iterator.next(); // 抛出异常 NoSuchElementException
        // 需要重置迭代器  iterator = list.iterator();
    }
    static class Book{
        private String name;
        private int price;

        public Book(String name, int price) {
            this.name = name;
            this.price = price;
        }

        @Override
        public String toString() {
            return "Book{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
        }
    }
}
遍历方式-for增强

特点:只能用于遍历集合和数组(简化版迭代器)

基本语法:

 for(元素类型 元素名:集合名或数组名){ 
     // 访问元素
 }

实例

public class ForS {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(new Toy("猛虎王",10));
        list.add(new Toy("霹雳火",20));
        list.add(new Toy("洛洛",15));

        // 增强for 本质仍然是迭代器
        // 集合
        for (Object b:list) {
            System.out.println(b.toString());
        }
        // 数组
        int[] num = {2,4,5,6};
        for (int n:num
             ) {
            System.out.println(n+"  ");
        }
    }
    static class Toy{
        private String name;
        private int price;

        public Toy(String name, int price) {
            this.name = name;
            this.price = price;
        }
        @Override
        public String toString() {
            return "Toy{" +
                    "name='" + name + '\'' +
                    ", price=" + price +
                    '}';
        }
    }
}
// debug 跳转 底层也是迭代器
// 跳转1
public Iterator<E> iterator() {
    return new Itr();
}
// 跳转2
public boolean hasNext() {
    return cursor != size;
}
// 跳转3
public E next() ......等等
遍历方式-普通for循环
public static void main(String[] args) {
    List list = new ArrayList();
    list.add("张三丰");
    list.add("秦天柱");
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}

Collection接口特征

  1. 可以实现存放多个元素,每个元素可以是Object类
  2. 没有直接实现的子类,都是通过它的子接口Set和List来实现的
Collection的常用方法
List list = new ArrayList();
// add 添加单个元素
list.add("lns");
list.add(520); // 自动装箱 new Integer(520)
list.add("zlr");
System.out.println(list.toString()); // [lns, 520, zlr]
/*
*  父类AbstractCollection: toString方法
*  public String toString() {
*    Iterator<E> it = iterator(); // 创建迭代器进行遍历该集合
*    if (! it.hasNext())
*        return "[]";
*
*    StringBuilder sb = new StringBuilder();
*    sb.append('[');
*    for (;;) {
*        E e = it.next();
*        sb.append(e == this ? "(this Collection)" : e);
*        if (! it.hasNext())
*            return sb.append(']').toString();
*         sb.append(',').append(' ');
*    }
*  }
*/

// remove 删除指定元素
list.remove(1); // 等价于 list.remove(520);
System.out.println(list); // [lns, zlr]

// contains 查找元素是否存在
System.out.println(list.contains("lns")); // true

// size 获取元素个数
System.out.println(list.size()); // 2

// isEmpty 判读是否为空
System.out.println(list.isEmpty()); // false

// clear 清空
list.clear();
System.out.println(list); // []

// addAll 添加多个元素
List list2 = new ArrayList();
list2.add("lns");
list2.add("love");
list2.add("zlr");
list.addAll(list2);
System.out.println(list); // [lns, love, zlr]

// containsAll 查找多个元素是否存在
System.out.println(list.containsAll(list2)); // true

// removeAll 删除多个元素
list.add("!");
list.removeAll(list2);
System.out.println(list); // [!]
List接口

Collection接口的子接口 (队列数据结构)

特点

  • List集合类元素顺序有序,且可以重复
  • List集合类中的每个元素都有其对应的顺序索引
// List集合类元素顺序有序,且可以重复
List list = new ArrayList();
list.add("das");
list.add("tom");
list.add("tom");
System.out.println(list); // [das, tom, tom]
// List集合类中的每个元素都有其对应的顺序索引
System.out.println(list.get(0));  // das
List常用方法
public class ListMethod {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("张三丰");
        list.add("秦天柱");
        // add 插入一个对象
        list.add(1,"lns");
        System.out.println(list); // [张三丰, lns, 秦天柱]

        List list2 = new ArrayList();
        list2.add("风火轮");
        list2.add("大黄蜂");
        // addAll 插入所有元素
        list.addAll(1,list2);
        System.out.println(list); // [张三丰, 风火轮, 大黄蜂, lns, 秦天柱]

        // indexOf 返回该对象首次出现的索引位置
        System.out.println(list.indexOf("lns")); // 3

        // lastIndexOf 返回该对象最后一次出现的索引位置
        list.add(1,"张三丰");
        System.out.println(list ); // [张三丰, 张三丰, 风火轮, 大黄蜂, lns, 秦天柱]
        System.out.println(list.lastIndexOf("张三丰")); // 1

        // set 替换对象数据
        list.set(1,"妞妞");
        System.out.println(list); // [张三丰, 妞妞, 风火轮, 大黄蜂, lns, 秦天柱]

        // subList 返回范围为[fromIndex,toIndex)位置的子集合 该方法返回的是子串集合的地址索引
        list = list.subList(0, 3);
        System.out.println(list); // [张三丰, 妞妞, 风火轮]

    }
}
ArrayList类

是由数组实现数据存储

image-20220227132949556

特点:

  • 元素可以是null,并且可以有多个

  • ArrayList是线程不安全的,不能在多线程的情况下使用

    // 对比这两种源码 区别在于是否线程安全  synchronized
    // ArrayList源码
    public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
    }
    // Vector源码
    public synchronized boolean add(E e) {
        modCount++;
        add(e, elementData, elementCount);
        return true;
    }
    
ArrayList源码分析

image-20220226184245606

  1. ArrayList类数据存储在Object类数组中(elementData)

    transient Object[] elementData; // transient 表示该属性不会被序列化
    
  2. 当使用无参构造方法创建该对象,初始elementData容量为0,第一次添加数据,扩容到10容量,以后每次扩容,则会扩大当前容量的1.5倍

  3. 如果使用指定大小的有参构造器,则初始elementData容量为指定大小,如果需要扩容,也是直接扩容1.5倍

扩容机制

无参构造器

1.设置断点

image-20220226200138044

2.debug跳转

// 跳转1
public boolean add(E e) {
    modCount++; // 记录集合被修改的次数  如果madCount的值因为线程原因意外改变,则抛出异常
    add(e, elementData, size); // e:传入的数据 elementDate:Object数组 size:数组元素数量
    return true;
}
// 跳转2
private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length) // s:数组元素数量 elementDate.length:数组容量 
        elementData = grow(); // 只有当容量够用,不会调用该方法
    elementData[s] = e; // 数组添加数据
    size = s + 1; 
}
// 跳转3
private Object[] grow() {
    return grow(size + 1); 
}
// 跳转4
private Object[] grow(int minCapacity) { // minCapacity:当前元素个数+1
    int oldCapacity = elementData.length; // 记录原容量
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA){ //判断容量是否为0
        //  传入newLength方法的参数:原容量,超出容量,0.5倍容量大小
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                  minCapacity - oldCapacity, oldCapacity >> 1 ); // >> 1 相当于乘0.5
        return elementData = Arrays.copyOf(elementData, newCapacity); // 扩容,保留原数据
    } else {
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }  // DEFAULT_CAPACITY 10   如果是容量为0,第一次扩容默认为10
}
// 当传入第11个数据时候跳转5
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
    // assert oldLength >= 0
    // assert minGrowth > 0

    int newLength = Math.max( minGrowth,prefGrowth ) + oldLength; // 当超出容量,则扩容1.5倍
    if (newLength - MAX_ARRAY_LENGTH <= 0) {
        return newLength;
    }
    return hugeLength(oldLength, minGrowth);
}

有参构造器

image-20220226202444863

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}
Vector类

是由数组实现数据存储

image-20220227132833785

Vector的基本介绍

// Vector的类定义
public class Vector<E>extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    
// Vector底层本质也是对象数组
protected Object[] elementData;

// Vector是线程安全
// 通常方法被 synchronized 关键字修饰

Vector源码分析

/*
 * The amount by which the capacity of the vector is automatically
 * incremented when its size becomes greater than its capacity.  If
 * the capacity increment is less than or equal to zero, the capacity
 * of the vector is doubled each time it needs to grow.
 * 当向量的大小变得大于其容量时,向量的容量自动增加的量。如果容量增量小于或等于零,
 * 则每次需要增长时,向量的容量都会增加一倍
 * @serial
 */
protected int capacityIncrement;

private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = ArraysSupport.newLength(oldCapacity,minCapacity - oldCapacity, 							capacityIncrement > 0 ? capacityIncrement : oldCapacity);
    return elementData = Arrays.copyOf(elementData, newCapacity);
}

public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
    // assert oldLength >= 0
    // assert minGrowth > 0

    int newLength = Math.max(minGrowth, prefGrowth) + oldLength; // 2倍
    if (newLength - MAX_ARRAY_LENGTH <= 0) {
        return newLength;
    }
    return hugeLength(oldLength, minGrowth);
}

Vector和ArrayList的比较

底层结构版本线程安全扩容机制
ArrayList可变数组jdk1.2不安全,但效率高如果有参构造 扩容1.5倍;如果是无参1.第一次默认10,第二次扩容1.5倍
Vector可变数组jdk1.0安全,但效率不高如果有参构造 扩容2倍;如果是无参1.第一次默认10,第二次扩容2倍
LinkedList类

LinkedList类基本介绍

  • LinkedList类底层实现了双向链表和双端队列特点
  • 可以添加任意元素包括null,并且可以重复
  • 线程不安全没有实现同步

image-20220227133039410

LinkedList类的底层结构

  1. 该类底层是一个双向链表

  2. 其中含有两个属性:first 和 last 分别指向首节点和尾节点

    image-20220227134230977

  3. 每个节点里面含有prev,next,item三个属性,其中通过prev指向前一个节点,通过next指向后一个节点,用item来存储数据

    image-20220227142526922

  4. 进行添加和删除操作,效率比数组高

添加数据源码分析

image-20220227160330222

// 添加第一个数据
// 跳转1
public boolean add(E e) { // 增加数据
    linkLast(e);
    return true;
}
// 跳转2
void linkLast(E e) {
    final Node<E> l = last; // last:null 第一次添加last为null
    final Node<E> newNode = new Node<>(l, e, null); //l:null e:"lns"  说明prev和next指向null
    last = newNode; // last 指向尾节点 
    if (l == null) // 添加第一个节点
        first = newNode; //  first和last都指向同一个节点
    else
        l.next = newNode; 
    size++;
    modCount++;
}
// 添加第二个数据: 省略部分不重要的
void linkLast(E e) {
    // 总结: l这个变量可以当成连接器,连接新节点和原来最后一个节点
    final Node<E> l = last; // l:链表的最后一个节点
    final Node<E> newNode = new Node<>(l, e, null); // 创建连接上一个节点的新节点,e:"zlr"
    last = newNode; // last指向新的节点(该节点就是新的最后一个节点)
    if (l == null)
        first = newNode;
    else 
        l.next = newNode; // l指向新节点:节点变成l的下一个节点
    size++;
    modCount++;
}
// 删除数据还是更改数据...等等看源码

常用方法

// add 增加节点
LinkedList linkedList = new LinkedList();
linkedList.add("lns");
linkedList.add("zlr");
System.out.println(linkedList.toString()); // [lns, zlr]

// remove 删除节点
linkedList.remove(); // 默认删除第一个节点
System.out.println(linkedList); // [zlr]

// set 修改节点
linkedList.set(0,"奥里给");
System.out.println(linkedList); // [奥里给]

// get 根据索引获得某个节点数据
System.out.println(linkedList.get(0)); // 奥里给

ArrayList和LinkedList的比较

底层结构增删效率改查效率
ArrayList可变数组低,数组扩容
LinkedList双向链表高,动态扩容
Set接口

public interface Set extends Collection

Set接口基本介绍

  • 无序(添加和取出顺序不一致),没有索引,因此该接口不能再使用普通for循环索引的方式遍历
  • 不允许重复数据,因此可以有null 但是只能有一个null
  • Set接口实现了Collection接口,所以可以使用该接口的所有方法
Set set = new HashSet();
set.add("lns");
set.add("null");
set.add("null"); // 只会存入一个数据
set.add("zlr");
System.out.println(set); // [lns, zlr, null] 无序
HashSet类

HashSet的底层实际上就是HashMap

/**
 * Constructs a new, empty set; the backing {@code HashMap} instance has
 * default initial capacity (16) and load factor (0.75).
 */
public HashSet() {
    map = new HashMap<>();
}

HashSet框架图

image-20220228105517461

特点:

  • 可以存放null值,但是只能存放一个

  • 不能存放重复元素

    public class HashSet01 {
        public static void main(String[] args) {
            Set set = new HashSet();
            System.out.println(set.add("lns")); // true
            System.out.println(set.add("lns")); // false
            System.out.println(set.add(new String("lns"))); // false 
            System.out.println("lns".hashCode() == new String("lns").hashCode()); // true
            System.out.println(set.add(new person("zlr"))); // true
            System.out.println(set.add(new person("zlr"))); // true
            // new String("lns") 的hashCode和"lns"相同
        }
    }
    class person{
        private String name;
    
        public person(String name) {
            this.name = name;
        }
    }
    
  • 元素是无序的,创建后顺序是固定的

HashSet底层机制

先介绍散列表:数组 + 链表

实例代码

public class HashSetStructure {
    public static void main(String[] args) {
        // 数组 + 链表
        Node[] table = new Node[16];
        // 创建节点
        Node node21 = new Node("lns", null);
        table[2] = node21;

        Node node22 = new Node("zlr", null);
        node21.next = node22;

        Node node23 = new Node("lp", null);
        node22.next = node23;

        Node node31 = new Node("cyj", null);
        table[3] = node31;

        System.out.println(Arrays.toString(table));
    }
}

class Node{ // 节点
    Object item; // 数据
    Node next; // 指向下个节点

    public Node(Object item, Node next) {
        this.item = item;
        this.next = next;
    }
}

图解

image-20220227201842892

用该图分析HashSet底层过程

  1. 添加一个元素时,先得到hash值 => 索引值(类似图中 0-16索引)

  2. 找到存储数据表table,看这个索引上是否有存放数据

    1. 如果没有找到,就直接加入(如图中的node21,node22,node23);如果有元素并且hashCode值相同,则调用equals方法进行比较,如果相同则不添加,反之添加
// 实例说明:如果有元素并且hashCode值相同,则调用equals方法进行比较,如果相同则不添加,反之添加
// 这就是为什么new String("lns")不会被添加以及为什么new person("zlr")会被添加2次
// 核心就在于equals方法
// 但是为什么"lns"的hashCode会和 new String("lns")相同呢(不是不同的地址吗)
// 关键就在String类重写了hashCode方法,字符串的hashCode是根据字符算出来的
/*
    String类计算hashCode的算法
    public static int hashCode(byte[] value) {
        int h = 0;
        for (byte v : value) { // value:传入的字符串
            h = 31 * h + (v & 0xff);
        }
        return h;
    }
*/
public class HashSet01 {
    public static void main(String[] args) {
        Set set = new HashSet();
        System.out.println(set.add("lns")); // true
        System.out.println(set.add("lns")); // false
        System.out.println(set.add(new String("lns"))); // false
        System.out.println(set.add(new person("zlr"))); // true
        System.out.println(set.add(new person("zlr"))); // true
    }
}

class person{
    private String name;

    public person(String name) {
        this.name = name;
    }
}
  1. 在jdk8中,一个链表的元素达到8个以及table数据表长度达到64.则将 数组+链表 => 红黑树
public class HashSet02 {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        hashSet.add("罗念笙");
        hashSet.add("张洛融");
        hashSet.add("罗念笙");
        System.out.println(hashSet); // [张洛融, 罗念笙]
/*
 *     分析源码
 *     假设传入"罗念笙"
 *     跳转1:add方法
 *     public boolean add(E e) {
 *         return map.put(e, PRESENT)==null; // PRESENT:占位 new Object()
 *     }
 *
 *     跳转2:put方法
 *     public V put(K key, V value) {
 *         return putVal(hash(key), key, value, false, true);
 *     }
 *
 *     跳转3:hash方法 计算hash值并返回给put方法中hash(key)
 *     static final int hash(Object key) {
 *         int h;
 *         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 *     }
 *
 *     跳转4:pubVal方法   传入形参:hash值, "罗念笙", PRESENT, false, true
 *     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
 *         Node<K,V>[] tab; Node<K,V> p; int n, i; // 定义辅助变量
 *         // transient Node<K,V>[] table; table:数组+链表形式
 *         if ((tab = table) == null || (n = tab.length) == 0) // 没有分配数组空间
 *             // 分配数组空间 newCap = DEFAULT_INITIAL_CAPACITY 默认16
 *             n = (tab = resize()).length; 
 *         if ((p = tab[i = (n - 1) & hash]) == null) // i = (n - 1) & hash 通过hash值计算索引
 *             tab[i] = newNode(hash, key, value, null); // 该索引下数组值为null,就直接添加节点
 *         else {
 *             Node<K,V> e; K k; // 定义辅助变量
 *             // 比较索引处首节点的 hash值 以及 指向地址是否相同 或者 比较值是否相同(重写的情况)
 *             if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
 *                 e = p;
 *             // 判断 索引p 指向的是否是红黑树
 *             else if (p instanceof TreeNode)
 *                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
 *             // 遍历该索引的所有节点,依次比较;如果相同则不添加
 *             else {
 *                 for (int binCount = 0; ; ++binCount) {
 *                     if ((e = p.next) == null) {
 *                         p.next = newNode(hash, key, value, null);
 *                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
 *                             treeifyBin(tab, hash);
 *                         break;
 *                     }
 *                     if (e.hash == hash &&
 *                         ((k = e.key) == key || (key != null && key.equals(k))))
 *                         break;
 *                     p = e;
 *                 }
 *             }
 *             if (e != null) { // existing mapping for key
 *                 V oldValue = e.value;
 *                 if (!onlyIfAbsent || oldValue == null)
 *                     e.value = value;
 *                 afterNodeAccess(e);
 *                 return oldValue;
 *             }
 *         }
 *         ++modCount;
 *         // threshold 阈值,用来提前给数组扩容 :threshold = (int)(DEFAULT_LOAD_FACTOR *                    DEFAULT_INITIAL_CAPACITY);
 *         // size 大小值得是加入的节点个数达到了阈值
 *         if (++size > threshold)
 *             resize();
 *         afterNodeInsertion(evict); // HashMap类留给子类继承使用的方法
 *         return null;
 *     }
 *
 *     // treeifyBin方法 一个链表的元素达到8个以及table数据表长度达到64.则将 数组+链表 => 红黑树
 *     final void treeifyBin(Node<K,V>[] tab, int hash) {
 *         int n, index; Node<K,V> e;
 *         // 如果表的长度(数组长度)小于64,则扩容
 *         // MIN_TREEIFY_CAPACITY:64  
 *         if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
 *             resize();
 *         // 如果表的长度(数组长度)大于等于64,则数组+链表 => 红黑树
 *         else if ((e = tab[index = (n - 1) & hash]) != null) {
 *             TreeNode<K,V> hd = null, tl = null;
 *             do {
 *                 TreeNode<K,V> p = replacementTreeNode(e, null);
 *                 if (tl == null)
 *                     hd = p;
 *                 else {
 *                     p.prev = tl;
 *                     tl.next = p;
 *                 }
 *                 tl = p;
 *             } while ((e = e.next) != null);
 *             if ((tab[index] = hd) != null)
 *                 hd.treeify(tab);
 *         }
 *     }
 *
 *  // 补充信息 非debug内容
 *  节点Node代码
 *  static class Node<K,V> implements Map.Entry<K,V> {
 *         final int hash;
 *         final K key;
 *         V value;
 *         Node<K,V> next;
 *
 *         Node(int hash, K key, V value, Node<K,V> next) {
 *             this.hash = hash;
 *             this.key = key;
 *             this.value = value;
 *             this.next = next;
 *         }
 *
 *         public final K getKey()        { return key; }
 *         public final V getValue()      { return value; }
 *         public final String toString() { return key + "=" + value; }
 *
 *         public final int hashCode() {
 *             return Objects.hashCode(key) ^ Objects.hashCode(value);
 *         }
 *
 *         public final V setValue(V newValue) {
 *             V oldValue = value;
 *             value = newValue;
 *             return oldValue;
 *         }
 *
 *         public final boolean equals(Object o) {
 *             if (o == this)
 *                 return true;
 *             if (o instanceof Map.Entry) {
 *                 Map.Entry<?,?> e = (Map.Entry<?,?>)o;
 *                 if (Objects.equals(key, e.getKey()) &&
 *                     Objects.equals(value, e.getValue()))
 *                     return true;
 *             }
 *             return false;
 *         }
 *     }
 */
    }
}
LinkedHahSet类

LinkedHashSet类底层是一个LinkedHashMap,是一个数组+双向链表的结构

// 初始化LinkedHashSet
// LinkedHashSet类构造器
public LinkedHashSet() {
    super(16, .75f, true); // 初始化容量16;负载因子0.75
}
// 调用父类HashSet构造器,初始化LinkedHashMap
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
// LinkedHashMap调用父类的HashMap的构造器
public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}
// 初始化HashMap
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY) // MAXIMUM_CAPACITY = 1 << 30
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

LinkedHashSet框架图

image-20220228105904095

特点:

  • 根据hashCode值来决定元素的存储位置,同时使用双向链表来维护元素的次序,所以在一定程度上是有序的
  • 不允许重复添加元素

LinkedHashSet类的底层分析

image-20220228170158121

// LinkedHashSet节点源码
// 继承Node节点(HashMap的静态内部类) 
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) {
        // Node节点构造器
        super(hash, key, value, next);
    }
}
// 实现Map接口里的Entry接口
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}
TreeSet类

基本介绍

TreeSet类本质就是调用TreeMap,源码,比较机制等会在TreeMap中详细述说

public TreeSet() {
    this(new TreeMap<>());
}

image-20220302162013910

Map接口

public interface Map<K, V> 与Collection接口并列存在

Map接口特点

  • 保存具有映射关系的数据:Key - Value
  • Map 中的Key - Value数据可以是任何的引用数据类型,会封装到HashMap$Node对象中
  • Map中的Key不允许重复,但是Value可以重复(并且数据都可以是null)
  • Key 和 Value存在一对一的关系,总是能通过Key找到对应得Value

image-20220228191506219

深入理解map接口的Node节点

// Node节点属于HashMap内部类,实现了Map.Entry<K,V>接口 ; 一个Node节点对象含有一个key和value
// Map.Entry<K,V> 为Map接口的内部接口
static class Node<K,V> implements Map.Entry<K,V> 
// key-value值是存储在Node节点对象中,Map.Entry中key-value指向Node节点对象的key-value值的引用(类似对象名对对象的引用)
final int hash;
final K key;
V value;
Node<K,V> next;
// 那么EntrySet又是什么呢
// Set接口都可理解就是单列集合,其实EntrySet就是存放Map.Entry数据类型的集合 
transient Set<Map.Entry<K,V>> entrySet;
// 代码示例
Map map = new HashMap();
map.put("name","罗念笙"); // key: name value: 罗念笙
map.put("name","张洛融"); // key值不能重复,如果重复会覆盖之前相同key值的value值
map.put("person","张洛融"); // value值能重复
System.out.println(map); // {person=张洛融, name=张洛融}
System.out.println(map.get("name")); // 张洛融 ; 能通过Key找到对应得Value
// 着重点看运行类型
Set set = map.entrySet();
System.out.println(set.getClass()); // class java.util.HashMap$EntrySet
for (Object obj: set
    ) {
    System.out.println(obj.getClass()); // class java.util.HashMap$Node
}

Map常用方法

// 常用方法
// put方法 添加key-value
Map map = new HashMap();
map.put("name","lns");
map.put("age",18);
map.put("grade",99);
map.put("grade",60); // 重复key,覆盖之前的value
System.out.println(map); // {grade=60, name=lns, age=18}
// remove方法 根据key值删除映射关系
map.remove("grade");
System.out.println(map); // {name=lns, age=18}
// get方法 根据key获得value值
System.out.println(map.get("age")); // 18
// size方法 获取元素个数
System.out.println(map.size()); // 2
// isEmpty方法 判断元素是否为空
System.out.println(map.isEmpty()); // false
// containsKey 查找该键是否存在
System.out.println(map.containsKey("name")); // true
// clear 清空键值对]
map.clear();
System.out.println(map); // {}

Map接口遍历方式

// Map接口遍历方式
Map map = new HashMap();
map.put("name","lns");
map.put("age",18);
map.put("grade",99);

//  第一组: 先取出key(keySet方法,通过key取出value (get方法)
Set set = map.keySet();
// 方式1: 增强for
for (Object key:set
     ) {
    // key: grade value: 99  key: name value: lns  key: age value: 18
    System.out.print("key: "+key+" value: "+map.get(key)+"  ");
}
System.out.println();
// 方式2: 迭代器
Iterator iterator = set.iterator();
while(iterator.hasNext()){
    Object key = iterator.next();
    // key: grade value: 99  key: name value: lns  key: age value: 18
    System.out.print("key: "+key+" value: "+map.get(key)+"  ");
}
System.out.println();

// 第二组: 通过EntrySet来获取key-value
Set entrySet = map.entrySet();
// 方式3: 用getKey方法 和 getValue方法
for (Object obj: entrySet
     ) {
    Map.Entry entry = (Map.Entry)obj;
    // key: grade value: 99  key: name value: lns  key: age value: 18
    System.out.print("key: "+entry.getKey()+" value: "+entry.getValue()+"  ");
}
System.out.println();
// 方式4: 迭代器
Iterator iterator1 = entrySet.iterator();
while(iterator1.hasNext()){
    Map.Entry entry = (Map.Entry)iterator1.next(); 
    // key: grade value: 99  key: name value: lns  key: age value: 18
    System.out.print("key: "+entry.getKey()+" value: "+entry.getValue()+"  ");
}
HashMap类

HashMap底层是数组+链表+红黑树

HashMap类特点

  • 保存具有映射关系的数据:Key - Value

  • Key不允许重复,但是Value可以重复(并且数据都可以是null); 如果重复,将会替换掉value值

    image-20220301171734335

  • Key 和 Value存在一对一的关系,总是能通过Key找到对应得Value

  • HashMap没有实现线程同步,是线程不安全的

HashMap框架图

image-20220301171028407

注意 HashMap扩容机制等价于HashSet扩容机制,如上述

Hashtable类

Hashtable类特点

  • 保存具有映射关系的数据:Key - Value

  • Hashtable的key和value都不允许是null,如果是,将会抛出空指针异常

    image-20220302085244120

  • Hashtable是线程安全的,与HashMap不同

    // Hashtable的put方法
    public synchronized V put(K key, V value)
    

Hashtable框架图

image-20220302135647398

Hashtable扩容机制

//Hashtable构造器初始化容量11
public Hashtable() {
    this(11, 0.75f); 
} 

// put方法
public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Hashtable.Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length; // 索引值的计算方式: 散列码(hash)% 散列表的长度(tab.length)
    @SuppressWarnings("unchecked") // 抑制警告
    Hashtable.Entry<K,V> entry = (Hashtable.Entry<K,V>)tab[index]; // 创建entry节点
    // 判断是否有相同的key的entry节点,如果有,就替换掉原来的value值;反之则添加entry节点
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }
    // 添加entry节点
    addEntry(hash, key, value, index);
    return null;
}

// addEntry方法
private void addEntry(int hash, K key, V value, int index) {
    Entry<?,?> tab[] = table; // 成员变量table: 用来存储之前添加的键值对
    if (count >= threshold) { // 判断是否需要扩容
        // Rehash the table if the threshold is exceeded
        rehash();

        tab = table;
        hash = key.hashCode();
        index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // Creates the new entry.
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>) tab[index];
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
    modCount++;
}

// rehash方法 用于扩容
protected void rehash() {
    int oldCapacity = table.length; // 记录原来的散列表的长度(table.length)
    Entry<?,?>[] oldMap = table; // 记录原来的散列表(table)

    // overflow-conscious code
    int newCapacity = (oldCapacity << 1) + 1; // 新容量 = 旧容量 * 2 + 1
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        if (oldCapacity == MAX_ARRAY_SIZE)
            // Keep running with MAX_ARRAY_SIZE buckets
            return;
        newCapacity = MAX_ARRAY_SIZE;
    }
    Entry<?,?>[] newMap = new Entry<?,?>[newCapacity]; // 数组扩容

    modCount++;
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    table = newMap;
    // 添加原来的数组数据
    for (int i = oldCapacity ; i-- > 0 ;) {
        for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
            Entry<K,V> e = old;
            old = old.next;

            int index = (e.hash & 0x7FFFFFFF) % newCapacity;
            e.next = (Entry<K,V>)newMap[index];
            newMap[index] = e;
        }
    }
}

细节说明

image-20220302155704520

// 源码说明
// addEntry方法
private void addEntry(int hash, K key, V value, int index) {
    Entry<?,?> tab[] = table; // 成员变量table: 用来存储之前添加的键值对
    if (count >= threshold) { // 判断是否需要扩容
        // Rehash the table if the threshold is exceeded
        rehash();

        tab = table;
        hash = key.hashCode();
        index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // Creates the new entry.
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>) tab[index]; // 该索引该的引用节点的引用赋值给e
    tab[index] = new Entry<>(hash, key, value, e); // 加入当前的节点,并且指向下一个节点e
    count++;
    modCount++;
}
// Entry内部类的构造器 说明e 为当前节点的下一个节点
protected Entry(int hash, K key, V value, Entry<K,V> next) {
    this.hash = hash;
    this.key =  key;
    this.value = value;
    this.next = next;
}

Hashtable类和HashMap类的区别

版本线程(安全)效率允许(null键 ,null值)
HashMap1.2不安全允许
Hashtable1.0安全较低不允许
TreeMap类

基本介绍

使用TreeMap时,如果是调用无参构造器,则放入的Key对象必须实现Comparable接口。StringInteger这些类已经实现了Comparable接口,因此可以直接作为Key使用。作为Value的对象则没有任何要求。如果作为Key的class没有实现Comparable接口,那么,必须在创建TreeMap时同时指定一个自定义排序算法

TreeMap类的有序是按一定规则的有序,而非LinkedHashSet的插入和取出顺序一致

注意:红黑树的具体结构,我会放在数据结构里详细介绍

TreeMap源码分析

  1. 无参构造器
// debug 代码示例
public class TreeMap01 {
    public static void main(String[] args) {
        TreeMap treeMap = new TreeMap();
        treeMap.put("name","罗念笙");
        treeMap.put("age",18);
        System.out.println(treeMap); // {age=18, height=192, name=罗念笙}
    }
}

// 无参构造器: new TreeMap();
public TreeMap() {
    comparator = null; // comparator:TreeMap中的属性(用来存储comparator内部类的对象)
}

// 传入第一个参数:put("name","罗念笙");
// 跳转1
public V put(K key, V value) { //key: name  value: 罗念笙
    return put(key, value, true);
}
// 跳转2
private V put(K key, V value, boolean replaceOld) {
    Entry<K,V> t = root; // t指向根节点
    if (t == null) { // 如果根节点没数据
        addEntryToEmptyMap(key, value); // 增加Entry节点
        return null;
    }
    // 删除的一部分代码,在添加第一个节点不会遍历
}
// 跳转3 
private void addEntryToEmptyMap(K key, V value) {
    compare(key, key); // type (and possibly null) check
    root = new Entry<>(key, value, null); // 创建新的节点
    size = 1;
    modCount++;
}
// 跳转4
final int compare(Object k1, Object k2) {
    // 如果构造器没有传入comparator内部类的对象,则k1对象对应的类必须实现了compareTo方法
    // 就是实现了Comparable接口并且重写compareTo方法,并且k1和k2是可以比较的
    return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
        : comparator.compare((K)k1, (K)k2);
}
// 跳转5  跳转到String类的compareTo方法
public int compareTo(String anotherString) {
    byte v1[] = value;
    byte v2[] = anotherString.value;
    byte coder = coder();
    if (coder == anotherString.coder()) {
        return coder == LATIN1 ? StringLatin1.compareTo(v1, v2)
            : StringUTF16.compareTo(v1, v2);
    }
    return coder == LATIN1 ? StringLatin1.compareToUTF16(v1, v2)
        : StringUTF16.compareToLatin1(v1, v2);
}

// 传入第二数:put("age",18);
// 跳转1
public V put(K key, V value) { // key: age  value: 18
    return put(key, value, true);
}
// 跳转2
private V put(K key, V value, boolean replaceOld) {
    Entry<K,V> t = root;
    if (t == null) { // 跳过
        addEntryToEmptyMap(key, value);
        return null;
    }
    // 辅助变量
    int cmp; 
    Entry<K,V> parent;
    // split comparator and comparable paths
    // 无参构造器中没有是实现该匿名内部类,所以comparator = null
    Comparator<? super K> cpr = comparator;
    if (cpr != null) { // 跳过
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else {
                V oldValue = t.value;
                if (replaceOld || oldValue == null) {
                    t.value = value;
                }
                return oldValue;
            }
        } while (t != null);
    } else {
        Objects.requireNonNull(key); // 检查key是否为null
        @SuppressWarnings("unchecked") // 抑制警告
        // 强转key为Comparable接口(接口多态):判断key是否实现了Comparable接口,如果不是将会抛出               ClassCastException异常
        Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t; // 根节点变成了父节点
            cmp = k.compareTo(t.key); // 比较规则
            if (cmp < 0) 
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else {
                V oldValue = t.value;
                if (replaceOld || oldValue == null) {
                    t.value = value;
                }
                return oldValue;
            }
        } while (t != null);
    }
    addEntry(key, value, parent, cmp < 0);
    return null;
}
// 跳转3 
public static <T> T requireNonNull(T obj) {
    if (obj == null)
        throw new NullPointerException();
    return obj;
}
// 跳转4 跳转到String类的compareTo方法
public int compareTo(String anotherString) {
    byte v1[] = value;
    byte v2[] = anotherString.value;
    byte coder = coder();
    if (coder == anotherString.coder()) {
        return coder == LATIN1 ? StringLatin1.compareTo(v1, v2)
            : StringUTF16.compareTo(v1, v2);
    }
    return coder == LATIN1 ? StringLatin1.compareToUTF16(v1, v2)
        : StringUTF16.compareToLatin1(v1, v2);
}
// 跳转5
private void addEntry(K key, V value, Entry<K, V> parent, boolean addToLeft) {
    Entry<K,V> e = new Entry<>(key, value, parent); // 创建新节点
    if (addToLeft)
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);
    size++;
    modCount++;
}
// 跳转6:设置节点颜色:红和黑
/** From CLR */
private void fixAfterInsertion(Entry<K,V> x) {
    x.color = RED;

    while (x != null && x != root && x.parent.color == RED) {
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateLeft(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateRight(parentOf(parentOf(x)));
            }
        } else {
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateRight(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    root.color = BLACK;
}

2.有参构造器

public class TreeMap02 {
    public static void main(String[] args) {
        // 通过key中的字符大小进行排序 降序: name > height > age
        TreeMap treeMap1 = new TreeMap(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                return ((car)o2).compareName((car)o1);
            }
        });
        treeMap1.put(new car("e"),"LNS");
        treeMap1.put(new car("da"),18);
        treeMap1.put(new car("dwes"),185);
        // {com.Al_tair.map_.treeMap_.car@27d6c5e0=185,
        //  com.Al_tair.map_.treeMap_.car@4f3f5b24=18,
        //  com.Al_tair.map_.treeMap_.car@15aeb7ab=LNS}
        System.out.println(treeMap1);
    }
}

class car{
    String name;
    public car(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public int compareName(Object o) {
        return this.getName().length()-((car)o).getName().length();
    }
}

Properties类

基本介绍

image-20220302141600899

Properties类特点

  • 保存具有映射关系的数据:Key - Value,注意键值对不需要有空格,值不需要用引号引起来,默认为String
  • Hashtable的key和value都不允许是null,如果是,将会抛出空指针异常

常用方法

// 常用方法
Properties properties = new Properties();
// put方法 添加数据,修改数据
properties.put("name","罗念笙");
properties.put("age",18);
properties.put("grade",99);
properties.put("grade",98); //修改数据
System.out.println(properties); // {grade=98, name=罗念笙, age=18}

// 通过key获取value
System.out.println(properties.get("name")); // 罗念笙

// remove方法 删除数据
properties.remove("grade");
System.out.println(properties); // {name=罗念笙, age=18}

load:加载配置文件的键值对到Properties对象
list:将数据显示到指定设备
getProperty(key):根据键获取值
getProperty(key,value):设置键值对到Properties对象
store:Properties中的键值对存储到配置文件中,如果含有中文,会存储为unicode码

Properties类的使用

public class Properties_ {
    public static void main(String[] args) throws IOException {
        // 读取Properties配置文件
        // 创建properties对象
        Properties pro = new Properties();
        // 加载数据到Properties对象中
        pro.load(new FileReader("E:\\Java_training\\Java_code\\JavaIdea03\\java\\Javase_HSping\\src\\com\\Al_tair\\ioStream_\\mysql.properties"));
        pro.list(System.out);
        String root = pro.getProperty("root");
        String pwd = pro.getProperty("pwd");
        System.out.println("root: " + root + " pwd: " + pwd);

        // 转存配置文件到mysql.properties
        Properties pro2 = new Properties();
        // 配置文件数据
        pro2.setProperty("charset","utf-8");
        pro2.setProperty("name","lns");
        pro2.setProperty("lover","zlr");
        // 存储到文件
        pro2.store(new FileWriter("E:\\Java_training\\Java_code\\JavaIdea03\\java\\Javase_HSping\\src\\com\\Al_tair\\ioStream_\\mysql2.properties"),null);
    }
}

image-20220317090850727

如何选择集合实现类

  1. 先判断存储的数据类型(一组对象或者一组键值对)
  2. 一组对象【单列集合】:Collection接口
    • 允许重复并且有序:List接口
      • 线程安全:Vector【底层是一个Object类型的可变数组】
      • 线程不安全:
        • 增删多:LinkedList【底层是一个双向链表】
        • 改查多:ArrayLIst【底层是一个Object类型的可变数组】
    • 不允许重复:Set接口
      • 无序并且线程不安全:HashSet【底层就是HashMap,数组+链表+红黑数】
      • 定制排序并且线程不安全:TreeSet
      • 插入和取出顺序一致,并且线程不安全: LinkedHashSet 【数组+双向链表】
  3. 一组键值对:Map接口
    • 线程不安全:
      • 键无序:HashMap类 【底层就是:数组+链表+红黑数】
      • 定制排序:TreeMap类
      • 键插入和取出顺序一致:LinkedHashMap类
    • 线程安全:读取配置文件:Properties类

Colleactions工具类

Collections是一个操作Set,List,Map等集合的工具类,提供了一系列静态的方法对集合元素进行排序,查找和修改等操作

// Collections工具类中常用方法
// 创建测试类
ArrayList arrayList = new ArrayList();
arrayList.add("cyj");
arrayList.add("lns");
arrayList.add("zlr");
System.out.println(arrayList); // [cyj, lns, zlr]
// reverse(List集合) 反转List中元素的顺序
Collections.reverse(arrayList);
System.out.println(arrayList); // [zlr, lns, cyj]
// shuffle(List集合) 对List集合元素进行随机排序
for (int i = 0; i < 3; i++) {
    Collections.shuffle(arrayList);
    System.out.println(arrayList); // 随机出现
}
// shuffle方法的源码
public static void shuffle(List<?> list) {
    Random rnd = r;
    if (rnd == null)
        r = rnd = new Random(); // harmless race.
    shuffle(list, rnd);
}
//  sort(List集合) 根据元素的自然排序对List集合进行升序 比如:字符串比较的是字符大小
arrayList.add("aaa");
Collections.sort(arrayList);
System.out.println(arrayList); // [aaa, cyj, lns, zlr]
// 用比较器Comparator自定义规则进行排序 sort(arrayList,new Comparator(){});
Collections.sort(arrayList,new Comparator(){
    @Override
    public int compare(Object o1, Object o2) {
        return ((String)o2).compareTo((String)o1);
    }
});
System.out.println(arrayList); // [zlr, lns, cyj, aaa]
// swap(List集合,int i,int j) 交换集合索引为i和j的位置
Collections.swap(arrayList,1,2);
System.out.println(arrayList); // [zlr, cyj, lns, aaa]
// max(Collection集合) 返回给定集合中自然排序最大的元素
System.out.println(Collections.max(arrayList)); // zlr
// max(Collection集合,new Comparator(){}) 返回给定集合中自治排序最大的元素
System.out.println(Collections.max(arrayList,new Comparator(){
    @Override
    public int compare(Object o1, Object o2) {
        return ((String)o2).compareTo((String)o1);
    }
})); // aaa
// frequency(Collection集合,集合中的元素) 返回该元素在集合中出现的频率
System.out.println(Collections.frequency(arrayList,"aaa")); // 1
// copy(List dest,List src) 复制src的元素到dest集合中
// 注意: 目标集合元素个数必须大于等于原来集合元素的个数
ArrayList dest = new ArrayList();
for (int i = 0; i < 6; i++) {
    dest.add(i);
}
Collections.copy(dest,arrayList);
System.out.println(dest); // [zlr, cyj, lns, aaa, 4, 5]

相关面试题

// 补充测试题
// 注意remove方法 是否能删除
HashSet hashSet = new HashSet();
Person person = new Person(1, "lns");
Person person2 = new Person(1, "zlr");
hashSet.add(person);
hashSet.add(person2);
System.out.println(hashSet); // [Person{id=1, name='lns'}, Person{id=1, name='zlr'}]
person.setName("4"); // 当重新设置名字,改变了hash值间接改变了索引,会导致接下来的删除不成功
hashSet.remove(person);
System.out.println(hashSet); // [Person{id=1, name='4'}, Person{id=1, name='zlr'}]

Java中有哪些容器(集合类)?
Java中的集合类主要由Collection和Map这两个接口派生而出,其中Collection接口又派生出三个子接口,分别是Set、List、Queue。所有的Java集合类,都是Set、List、Queue、Map这四个接口的实现类,这四个接口将集合分成了四大类,其中

Set代表无序的,元素不可重复的集合;

List代表有序的,元素可以重复的集合;

Queue代表先进先出(FIFO)的队列;

Map代表具有映射关系(key-value)的集合。

这些接口拥有众多的实现类,其中最常用的实现类有HashSet、TreeSet、ArrayList、LinkedList、ArrayDeque、HashMap、TreeMap等。

Java中的容器,线程安全和线程不安全的分别有哪些?

java.util包下的集合类大部分都是线程不安全的,例如我们常用的HashSet、TreeSet、ArrayList、LinkedList、ArrayDeque、HashMap、TreeMap,这些都是线程不安全的集合类,但是它们的优点是性能好。如果需要使用线程安全的集合类,则可以使用Collections工具类提供的synchronizedXxx()方法,将这些集合类包装成线程安全的集合类。

java.util包下也有线程安全的集合类,例如Vector、Hashtable。这些集合类都是比较古老的API,虽然实现了线程安全,但是性能很差。所以即便是需要使用线程安全的集合类,也建议将线程不安全的集合类包装成线程安全集合类的方式,而不是直接使用这些古老的API。

从Java5开始,Java在java.util.concurrent包下提供了大量支持高效并发访问的集合类,它们既能包装良好的访问性能,有能包装线程安全。这些集合类可以分为两部分,它们的特征如下:

  • 以Concurrent开头的集合类:

    以Concurrent开头的集合类代表了支持并发访问的集合,它们可以支持多个线程并发写入访问,这些写入线程的所有操作都是线程安全的,但读取操作不必锁定。以Concurrent开头的集合类采用了更复杂的算法来保证永远不会锁住整个集合,因此在并发写入时有较好的性能。

  • 以CopyOnWrite开头的集合类:

    以CopyOnWrite开头的集合类采用复制底层数组的方式来实现写操作。当线程对此类集合执行读取操作时,线程将会直接读取集合本身,无须加锁与阻塞。当线程对此类集合执行写入操作时,集合会在底层复制一份新的数组,接下来对新的数组执行写入操作。由于对集合的写入操作都是对数组的副本执行操作,因此它是线程安全的。

描述一下Map put的过程

如上述我的HashMap和Hashtable的与添加源码分析

img

如何得到一个线程安全的Map?

  1. 使用Collections工具类,将线程不安全的Map包装成线程安全的Map;
  2. 使用java.util.concurrent包下的Map,如ConcurrentHashMap;
  3. 不建议使用Hashtable,虽然Hashtable是线程安全的,但是性能较差。

说一说你对LinkedHashMap的理解

LinkedHashMap使用双向链表来维护key-value对的顺序(其实只需要考虑key的顺序),该链表负责维护Map的迭代顺序,迭代顺序与key-value对的插入顺序保持一致。

LinkedHashMap可以避免对HashMap、Hashtable里的key-value对进行排序(只要插入key-value对时保持顺序即可),同时又可避免使用TreeMap所增加的成本。

LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能。但因为它以链表来维护内部顺序,所以在迭代访问Map里的全部元素时将有较好的性能。

请介绍TreeMap的底层原理

TreeMap基于红黑树(Red-Black tree)实现。映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。TreeMap的基本操作containsKey、get、put、remove方法,它的时间复杂度是log(N)。

TreeMap包含几个重要的成员变量:root、size、comparator。其中root是红黑树的根节点。它是Entry类型,Entry是红黑树的节点,它包含了红黑树的6个基本组成:key、value、left、right、parent和color。Entry节点根据根据Key排序,包含的内容是value。Entry中key比较大小是根据比较器comparator来进行判断的。size是红黑树的节点个数。

泛型

基本介绍

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。

泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

泛型特点

  • 可以使用任意字母A-Z T 是 type的缩写,比较常用

    // 自定义泛型
    class Template<E>{
        E filed;
    
        @Override
        public String toString() {
            return "Template{" +
                    "filed=" + filed +
                    '}';
        }
    
        public E method(){
            return filed;
        }
    
        public Template(E filed) {
            this.filed = filed;
        }
    }
    
  • 只能指代引用类型的数据

  • 在给泛型指定具体类型后,可以传入该类型或者其子类类型

    public class Generic02 {
        public static void main(String[] args) {
            C<A> C = new C<A>(new A());
            C<A> C2 = new C<A>(new B());
        }
    }
    class A{}
    class B extends A{}
    class C<E>{
        E c;
    
        public C(E c) {
            this.c = c;
        }
    } 
    
  • 泛型的使用形式

    ArrayList<Integer> arrayList = new ArrayList<Integer>();
    ArrayList<Integer> arrayList2 = new ArrayList<>(); // 推荐
    ArrayList arrayList = new ArrayList(); // 默认泛型为Object
    

分析泛型编译后的文件

java里面的泛型只存在于源代码里面,一旦经过编译之后,所有的泛型都会被擦除掉,全部被替换为原来的裸类型,并在对元素进行访问和修改的时候,才会加上强制类型转换。

// (所谓的裸类型指的是,ArrayList<Integer>() 他的裸类型就是ArrayList())
public class Test {
    public static void main(String[] args) {
        ArrayList<String> sList = new ArrayList<String>();
        ArrayList<Integer> iList = new ArrayList<Integer>();
        // getClass()方法 表示此对象的运行时类的Class对象
        System.out.println(sList.getClass() == iList.getClass()); // true
    }
}
// 代码
public class Generic02 {
    public static void main(String[] args) {
        C<A> C = new C<A>(new A());
        C<A> C2 = new C<A>(new B());
        A a = C.c;
    }
}
class A{}
class B extends A{}
class C<E>{
    E c ;
    E[] e;
    public C(E c) {
        this.c = c;
    }
}
// 反编译代码  IDEA 选中代码 -> View -> Show Bytecode 或者 cmd 输入 javap -c XXX.class
// 我删除了一些不必要的代码
public class com/Al_tair/generic_/Generic02 {
  // 省略始化Generic02类
  public static main([Ljava/lang/String;)V
   L0  //   C<A> C = new C<A>(new A());
    LINENUMBER 9 L0
    NEW com/Al_tair/generic_/C 
    DUP 
    NEW com/Al_tair/generic_/A 
    DUP 
    INVOKESPECIAL com/Al_tair/generic_/A.<init> ()V
    // Ljava/lang/Object; (L开头 内容是对象 ;结尾 ) 传入是Object对象
    // 原本默认传进去就是Object类的对象,使用的时候使用自动强转换成对应传入的类型(现象如下)
    INVOKESPECIAL com/Al_tair/generic_/C.<init> (Ljava/lang/Object;)V 
    ASTORE 1
   L1  //   C<A> C2 = new C<A>(new B());
    LINENUMBER 10 L1
    NEW com/Al_tair/generic_/C
    DUP
    NEW com/Al_tair/generic_/B
    DUP
    INVOKESPECIAL com/Al_tair/generic_/B.<init> ()V
    INVOKESPECIAL com/Al_tair/generic_/C.<init> (Ljava/lang/Object;)V
    ASTORE 2
   L2
    LINENUMBER 11 L2
    RETURN
    GETFIELD com/Al_tair/generic_/C.c : Ljava/lang/Object;
    // 此处有一个checkcast指令,checkcast 用于检查类型强制转换是否可以进行,也就是泛型在获取值的时候进行了强制类型转换。
    CHECKCAST com/Al_tair/generic_/A
   L3
    ......
}                            

类型擦除的缺点

1.使用类型擦除直接导致了对于原始的数据类型无法支持,比如int,long这种,因为java不支持Object类型和基本数据类型之间的强制类型转换,也就是说一旦类型擦除之后,就没法在进行 类型转换了。也正是这样,现在的泛型都是不支持原始类型的,比如ArrayList,而不能使用ArrayList。

2.运行期间无法获得泛型类型信息。因为泛型都被擦除了,都被替换成了裸类型。这样就导致了下面的程序都会报错,比如无法使用泛型来创建对象,或者数组。

自定义泛型

自定义泛型类

基本语法

class 类名<泛型>{}

注意细节

  • 泛型类的类型是在创建对象时确定的(因为创建对象时,需要指定确定的类型)所以在类加载就创建的成员无法使用泛型

    • 普通成员可以使用泛型(属性和方法)但是成员变量不能赋值

    • 静态方法中不能使用类的泛型

    • 使用泛型的数组不能直接初始化 不能初始化的原因

      //  E[] e = new E[3]; 报错
      
自定义泛型接口

基本语法

interface 接口名<泛型>{}

注意细节

  • 静态成员中不能使用类的泛型
  • 泛型接口的类型是在实现接口的时候确定的
  • 没有指定类型,则默认为Object类型
自定义方法

基本语法

// 一般参数列表和泛型对应
修饰符<泛型>返回类型 方法名(参数列表){} 
// 以下非泛型方法,而是使用了泛型
public void XXX(E e){}

注意细节

  • 泛型方法可以放在普通类或者泛型类中
  • 方法在使用之前,类型必须确定

泛型的继承和同配符

  • 泛型不具有继承性
  • <?> : 支持任意类型
  • <? extends A> :支持A 类以及A类的子类,规定了泛型的上限
  • <> super A> ;支持A类以及A类的父类,规定了泛型的下限

相关面试题

什么是泛型?泛型的作用?

  • Java 泛型(Generics)是 JDK 5 中引入的一个新特性。
  • 使用泛型参数,可以增强代码的可读性以及稳定性。编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList<Persion> persons = new ArrayList<String>() 这行代码就指明了该ArrayList 对象只能传入 Persion 对象,如果传入其他类型的对象就会报错。
  • 可以用于构建泛型集合。原生 List 返回类型是 Object ,需要手动转换类型才能使用,使用泛型后编译器自动转换。

推荐面试题

线程

进程和线程的概念

概念:进程是指运行中的程序,是程序的一次执行过程或是正在运行的一个程序。动态过程:产生,存在,消亡的过程

image-20220306182410060

那么线程是什么?

线程由进程创建,是进程的一个实体,一个进程可以拥有多个线程

  • 单线程:同一个时刻只允许一个线程
  • 多线程:同一个时刻,可以执行多个线程

并发:同一个时刻,多个任务交替执行,单核cpu实现多任务

并行:同一个时刻,多个任务同时执行,多核cpu可以实现并行执行多任务

那我们为什么要用多线程而不是多进程呢?

线程间的切换和调度成本远小于进程

线程的生命周期

public enum State {
    // 创建进程,但是资源条件未满足
    NEW,
    // 运行进程
    RUNNABLE,
    // 阻塞进程
    BLOCKED,
    // 无时间限制等待notify()方法唤醒
    WAITING,
    // 有时间限制等待notify()方法唤醒
    TIMED_WAITING,
    // 结束进程
    TERMINATED;
}

image-20220309092226604

image-20220309205604317

线程基本使用

创建线程的两种方式
  1. 继承Thread类,重写run方法(本质:Thread类也实现了Runable接口)

  2. 实现Runable接口,重写run方法

    // 使用Thread构造接受实现了Runnable的类,可以调用start()方法
    public Thread(Runnable target) {
        this(null, target, "Thread-" + nextThreadNum(), 0);
    }
    

image-20220306191925689

源码解析多线程机制

多线程机制说明

image-20220307111422720

用例代码

// 疑问:为什么调用start()方法而不是直接调用run()方法,不都是实现run()方法吗?
// 本质区别有没有创建新的线程,直接调用run方法就是和使用普通方法一样没什么区别,并没有创建线程
public class Thread01 extends Thread{
    int times = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread01 thread01 = new Thread01();
        thread01.start();

        for (int i = 0; i < 60; i++) {
            System.out.println(Thread.currentThread().getName()+i);
            Thread.sleep(1000);
        }
    }

    @Override
    public void run() {
        while(true){
            try {
                Thread.sleep(1000);
            }catch (Exception e){
                System.out.println(e.getMessage());
            }
            System.out.println("喵喵,我是小猫咪"+ ++times );
            if(times == 80){
                break;
            }
        }
    }
}

使用Terminal – jconsole工具观察进程

注意要main方法和其他进程要持续较长时间。这样子才好观测

image-20220307112032227

源码分析

image-20220307194551556

// 调用线程start方法:thread01.start();
// 源码分析
public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    group.add(this);
    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
        if (!started) {
            group.threadStartFailed(this);
        }
        } catch (Throwable ignore) {

        }
    }
}

// 本地方法,开辟线程
private native void start0();

进程终止

stop()方法(不推荐)

为什么stop()方法被废弃而不推荐使用呢?

因为stop()方法太过于暴力,强行把执行到一半的程序强行退出,会导致数据不一致的问腿

自制设置标志位退出
public class StopThread {
    public static void main(String[] args) throws InterruptedException {
        Thread1 thread1 = new Thread1();
        thread1.start();
        Thread.sleep(10000);
        thread1.setFlag(false);
    }
}

class Thread1 extends Thread{
    private int count = 0;
    // 设置标志位来判断线程终止时间
    private boolean flag = true;
    @Override
    public void run() {
        while (flag){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(++count);
        }
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}
中断方式退出程序

中断方式类似于之前通过标志位方式退出线程的方法,但是中断更加强劲一些,它可以让等待在sleep或者wait的线程引发异常退出

public class InterruptThread {
    public static void main(String[] args) throws InterruptedException {
        Interrupt thread1 = new Interrupt();
        thread1.start();
        Thread.sleep(10000);
        thread1.interrupt();
    }
}

class Interrupt extends Thread{
    private int count = 0;
    private boolean flag = true;
    @Override
    public void run() {
        while (true){
            if(this.isInterrupted()){
                System.out.println("Interrupted");
                break;
            }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
                // 为什么这里还需要中断一次
                // 因为sleep()方法中断抛出的异常会清除中断标志位,因此还需要再中断一次
                this.interrupt();
            }
            System.out.println(++count);
        }
    }
}

线程的常用方法

等待wait()和通知notify()

有些人会好奇,wait和notify方法不是Object类的方法吗,为什么放在线程这里特别拿出来讲?

因为这两个方法平时并不能随便调用,它必须包含在对应的同步块中

public final void wait() throws InterruptedException {
    wait(0L);
}
// 当多个线程在等待,则随机通知其中一个等待线程
public final native void notify();
// 通知所有等待线程
public final native void notifyAll();

Object.wait()方法和Thread.sleep()方法的区别

  • wait()方法可以被唤醒,使用wait方法之后会释放目标对象的锁

  • sleep()方法不会释放任何的资源

等待线程结束join()

join()方法:线程的插队,如果插队的线程一旦插入成功,则肯定先执行完插入的线程的所有任务、

// 无线等待,直到目标线程的任务执行完成
public final void join() throws InterruptedException
// 给出一个最大的等待时间
public final synchronized void join(long millis, int nanos) throws InterruptedException

示例代码

public class Join {
    public static void main(String[] args) throws InterruptedException {
        Thread3 t = new Thread3();
        t.start();
        for (int i = 1; i <= 20; i++) {
            if(i == 5){
                System.out.println("让Thread3先完成");
                t.join();
                System.out.println("main继续执行");
            }
            System.out.println("main: " + i);
            Thread.sleep(1000);
        }
    }
}

class Thread3 extends Thread{
    private int count;
    @Override
    public void run() {
        while(true){
            if(count++ == 20){
                System.out.println("Thread3结束了");
                break;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread3: "+count);
        }
    }
}

join()方法的底层源码

// join方法的本质就是调用wait方法在当前对象实例进行等待
// 被等待的线程会在执行完成后调用notifyAll()方法唤醒等待的进程进程
public final synchronized void join(final long millis)
    throws InterruptedException {
    if (millis > 0) {
        if (isAlive()) {
            final long startTime = System.nanoTime();
            long delay = millis;
            do {
                wait(delay);
            } while (isAlive() && (delay = millis -
                   TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
        }
    } else if (millis == 0) 
        // 测试此线程是否存在。如果线程已启动且尚未死亡,则该线程处于活动状态 RUNNABLE状态
        while (isAlive()) {
            wait(0);
        }
    } else {
        throw new IllegalArgumentException("timeout value is negative");
    }
}
谦让yeild()

yeild():线程的礼让,让出cpu让其他进程执行,但是礼让的时间不确定,也不一定礼让成功,还有就是当前处理器是否忙碌,如果处理器完成处理的过来,就不会进行礼让

使用场景:当你觉得这个线程不重要或者优先级很低,那适当让出cpu给那些更重要的线程是否是一个明智之举

用户线程和守护线程

用户线程:又称工作线程,当执行的任务执行完或通知方式结束

守护线程:一般是为工作线程服务,当所有线程结束,守护线程自动结束(比如:垃圾回收机制)

public class ThreadMethod {
    public static void main(String[] args) {
        MyDaemonThread md  = new MyDaemonThread();
        // 设置为守护线程
        md.setDaemon(true);
        md.start();
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.getMessage();
            }
            System.out.println("用户线程在此");
        }
    }
}

class MyDaemonThread extends Thread{
    @Override
    public void run() {
        while(true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.getMessage();
            }
            System.out.println("守护线程在此");
        }
    }
}

线程同步机制

同步概念:当有多个线程同时在对内存进行操作,在某一个时刻只允许一个线程对该内存进行操作(比如:写操作)

关键字synchronized的作用是实现线程的同步,它的工作是对同步代码枷锁,使得每一次只能有一个线程进入同步块,从而保证了线程的安全

关键字synchronized的用法

// 指定锁对象  默认锁对象就是this
synchronized(对象){} // 需要得到对象的锁,才能操作同步代码
// 直接作用于实例方法  默认锁对象就是this
public synchronized void method(){}
// 直接作用于静态方法  默认锁对象就是当前类.class
public static synchronized void method(){}
public class increase01 implements Runnable{
    static int count = 0;
    static int count2 = 0;

    public static synchronized  void increase(){
        count++;
    }
    public  synchronized  void increase2(){
        count2++;
    }
    public static void main(String[] args) throws InterruptedException {
        // 如果同一类传入的对象不同,对象锁就无法启到作用了,必须使用类的锁才可以锁住
        Thread t1 = new Thread(new increase01());
        Thread t2 = new Thread(new increase01());
        t1.start();t2.start();
        t1.join(); t2.join();
        System.out.println(count); // 20000000
        System.out.println(count2); // 小于20000000
        
        i
        System.out.println("-------");
        increase01.count2 = 0;
        increase01.count = 0;
        // 传入了相同对象,就不需要使用静态锁,对象锁就可以实现
        increase01 inc = new increase01();
        Thread thread = new Thread(inc);
        Thread thread1 = new Thread(inc);
        thread.start(); thread1.start();
        thread.join(); thread1.join();
        System.out.println(count);  // 20000000
        System.out.println(count2); // 20000000
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000000; i++) {
            increase();
        }
    }
}

释放锁

四种情况释放锁

  • 当前线程的同步方法和同步代码块执行结束
  • 当前的线程在同步代码块和同步方法中遇到break,return
  • 当前线程在同步代码块中出现了未处理的Error或者Exception导致被迫退出
  • 当前的线程在同步代码块或者同步方法中执行了wait房啊,暂停当前的线程同时释放资源

二种情况不释放锁

  • 线程执行同步代码块或者同步方法时,程序调用Thread.sleep()和Thread.yield()方法不会释放锁
  • 线程执行同步代码块或者同步方法时,其他线程调用suspend方法将它挂起,此时它并不会释放该锁(不推荐使用挂起方法)

编程题

1.在main方法中启动两个线程,在第一个线程循环随机打印100 以内的整数,直到第二个线程从键盘中读取了‘Q’命令就终止了第一个线程

public class Homework01 {
    public static void main(String[] args) throws InterruptedException {
        RandomNum randomNum = new RandomNum();
        Thread thread = new Thread(randomNum);
        Thread thread2 = new Thread(new Input(randomNum));
        thread.start();
        thread2.start();
    }
}

// 线程1
class RandomNum implements Runnable{
    private boolean loop = true;

    public void setLoop(boolean loop) {
        this.loop = loop;
    }

    @Override
    public void run() {
        while(loop){
            System.out.println((int)(Math.random()*100));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.getMessage();
            }
        }
        System.out.println("RandomNum退出程序");
    }
}

// 线程2
class Input implements Runnable{

    private  RandomNum r;

    public Input(RandomNum r) {
        this.r = r;
    }

    @Override
    public void run() {
        Scanner sc = new Scanner(System.in);
        while(true){
            char c = sc.nextLine().charAt(0);
            if(c == 'Q'){
                r.setLoop(false);
            }
            System.out.println(c);
            if()
        }
    }
}

2.有两个用户分别从同一个卡上取钱(总金额10000).每次只能取1000元,当金额不足够时,就不能取款了,不能出现超额取款

public class Homework02 {
    public static void main(String[] args) {
        // 同一个对象 指代的是从同一个卡上取款
        withdrawals withdrawals = new withdrawals();
        // 不用线程指代的是不用用户取款
        Thread user1 = new Thread(withdrawals);
        Thread user2 = new Thread(withdrawals);
        user1.start();
        user2.start();
    }
}

class withdrawals implements Runnable{
    static int moneySum = 10000;
    @Override
    public void run() {
        while(true){synchronized(this){
            if(moneySum>= 1000){
                moneySum -= 1000;
                System.out.println(Thread.currentThread().getName() +": 取款1000元 ");
                System.out.println("余额剩余: "+moneySum);
            }else{
                System.out.println("余额不足...");
                break;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }}
    }
}

相关面试题

1.创建线程有哪几种方式?

创建线程有三种方式,分别是继承Thread类、实现Runnable接口、实现Callable接口。

  • 通过继承Thread类来创建并启动线程的步骤如下:
    1. 定义Thread类的子类,并重写该类的run()方法,该run()方法将作为线程执行体。
    2. 创建Thread子类的实例,即创建了线程对象。
    3. 调用线程对象的start()方法来启动该线程。
  • 通过实现Runnable接口来创建并启动线程的步骤如下:
    1. 定义Runnable接口的实现类,并实现该接口的run()方法,该run()方法将作为线程执行体。
    2. 创建Runnable实现类的实例,并将其作为Thread的target来创建Thread对象,Thread对象为线程对象。
    3. 调用线程对象的start()方法来启动该线程。
  • 通过实现Callable接口来创建并启动线程的步骤如下:
    1. 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值。然后再创建Callable实现类的实例。
    2. 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
    3. 使用FutureTask对象作为Thread对象的target创建并启动新线程。
    4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

2.线程是否可以重复启动,会有什么后果?

只能对处于新建状态的线程调用start()方法,否则将引发IllegalThreadStateException异常。

扩展阅读

当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。

当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。

3. 如何实现线程同步?

  1. 同步方法

    即有synchronized关键字修饰的方法,由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。需要注意, synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。

  2. 同步代码块

    即有synchronized关键字修饰的语句块,被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。需值得注意的是,同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

  3. ReentrantLock

    Java 5新增了一个java.util.concurrent包来支持同步,其中ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。需要注意的是,ReentrantLock还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,因此不推荐使用。

  4. volatile

    volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值。需要注意的是,volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。

  5. 原子变量

    在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。例如AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer。可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。

4.说一说Java多线程之间的通信方式

  1. wait()、notify()、notifyAll()

    如果线程之间采用synchronized来保证线程安全,则可以利用wait()、notify()、notifyAll()来实现线程通信。这三个方法都不是Thread类中所声明的方法,而是Object类中声明的方法。原因是每个对象都拥有锁,所以让当前线程等待某个对象的锁,当然应该通过这个对象来操作。并且因为当前线程可能会等待多个线程的锁,如果通过线程来操作,就非常复杂了。另外,这三个方法都是本地方法,并且被final修饰,无法被重写。

    wait()方法可以让当前线程释放对象锁并进入阻塞状态。notify()方法用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。notifyAll()用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。

    每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了已就绪(将要竞争锁)的线程,阻塞队列存储了被阻塞的线程。当一个阻塞线程被唤醒后,才会进入就绪队列,进而等待CPU的调度。反之,当一个线程被wait后,就会进入阻塞队列,等待被唤醒。

  2. await()、signal()、signalAll()

    如果线程之间采用Lock来保证线程安全,则可以利用await()、signal()、signalAll()来实现线程通信。这三个方法都是Condition接口中的方法,该接口是在Java 1.5中出现的,它用来替代传统的wait+notify实现线程间的协作,它的使用依赖于 Lock。相比使用wait+notify,使用Condition的await+signal这种方式能够更加安全和高效地实现线程间协作。

    Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition() 。 必须要注意的是,Condition 的 await()/signal()/signalAll() 使用都必须在lock保护之内,也就是说,必须在lock.lock()和lock.unlock之间才可以使用。事实上,await()/signal()/signalAll() 与 wait()/notify()/notifyAll()有着天然的对应关系。即:Conditon中的await()对应Object的wait(),Condition中的signal()对应Object的notify(),Condition中的signalAll()对应Object的notifyAll()。

  3. BlockingQueue

    Java 5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但它的主要用途并不是作为容器,而是作为线程通信的工具。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。

    程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。线程之间需要通信,最经典的场景就是生产者与消费者模型,而BlockingQueue就是针对该模型提供的解决方案。

5.说一说sleep()和wait()的区别

  1. sleep()是Thread类中的静态方法,而wait()是Object类中的成员方法;
  2. sleep()可以在任何地方使用,而wait()只能在同步方法或同步代码块中使用;
  3. sleep()不会释放锁,而wait()会释放锁,并需要通过notify()/notifyAll()重新获取锁。

IO流

文件

文件就是保存数据的地方

文件流

文件在程序中是以流的方式来操作的

image-20220313101045337

常见文件的操作

// 构造器
File(File parent, String child)  // 从父抽象路径名和子路径名字符串创建新的File实例
// 代码实现
File parentFile = new File("C:\\Users\\Ushop\\Desktop\\JavaLoad");
String fileName = "w.txt";
File file = new File(parentFile, fileName);
file.createNewFile();
    
File(String pathname)  // 通过将给定的路径名字符串转换为抽象路径名来创建新的File实例
// 代码实现
String filePath = "C:\\Users\\Ushop\\Desktop\\JavaLoad\\w.txt";
File file = new File(filePath);
file.createNewFile();    

File(String parent, String child) // 从父路径名字符串和子路径名字符串创建新的File实例
// 代码实现
String parentFile = "C:\\Users\\Ushop\\Desktop\\JavaLoad";
String fileName = "w.txt";
File file = new File(parentFile, fileName);
file.createNewFile();

常见文件信息

public static void main(String[] args) throws IOException {
    // 目录可以理解为文件夹
    String filePath = "C:\\Users\\Ushop\\Desktop\\JavaLoad\\w.txt";
    File file = new File(filePath);
    System.out.println("文件名: "+file.getName()); // 文件名: w.txt
    System.out.println("绝对路劲: "+file.getAbsolutePath()); // 绝对路劲: C:\Users\Ushop\Desktop\JavaLoad\w.txt
    System.out.println("父级路径: "+file.getParent()); // 父级路径: C:\Users\Ushop\Desktop\JavaLoad
    System.out.println("文件大小: "+file.length()); // 0
    System.out.println("文件是否存在: "+file.exists()); // false
    file.createNewFile();
    System.out.println("文件是否存在: "+file.exists()); // true
}

文件夹的使用

// 注意文件和文件夹的细微区别
public static void main(String[] args) throws IOException {
    String filePath = "C:\\Users\\Ushop\\Desktop\\JavaLoad\\test";
    File file = new File(filePath);
    file.createNewFile(); // 创建文件
    file.mkdir(); // 创建单极目录文件夹
    file.mkdirs(); // 创建多级目录的文件夹
    // 删除文件或者文件夹
    if(file.exists()){
        if(file.delete()){
            System.out.println("删除成功");
        }else{
            System.out.println("删除失败");
        }
    }else{
        System.out.println("该文件不存在");
    }
}

IO流原理及流的分类

image-20220313160149525

IO流用于处理数据传输,如读写文件或者网络通讯

流的分类(流的本质就是运输者:运输文件数据)

  • 按操作数据单位不同分为:字节流(8bit),字符流(按字符)

  • 按数据流的流向不同分为:输入流和输出流

  • 按流的角色不同分为:节点流,处理流/包装流

    抽象基类字节流字符流
    输入流InputStreamReader
    输出流OutputStreamWriter

IO体系图中的常用类

字节流
InputStream:字节输入流
FileInputStream

image-20220313195635130

// 文件输入流输入以字节的方式
// 当前存在问题:无法解决中文乱码的问题
String fileName = "C:\\Users\\Ushop\\Desktop\\JavaLoad\\test\\hello.txt";
FileInputStream fis = new FileInputStream(fileName);
int data = 0;
while((data = fis.read()) != -1){
    System.out.print((char)data);
}
fis.close();

// 优化后: 读取速度加快
String fileName = "C:\\Users\\Ushop\\Desktop\\JavaLoad\\test\\hello.txt";
FileInputStream fis = new FileInputStream(fileName);
int readLen = 0;
// 字节数组 一次读取8字节
byte[] bytes = new byte[8];
// 读入缓冲区的总字节数,如果没有更多数据,则返回-1
while((readLen = fis.read(bytes)) != -1){
    System.out.print(new String(bytes,0,readLen));
}
fis.close();

// 读操作是本地方法
private native int readBytes(byte b[], int off, int len) throws IOException;
BufferedInputStream
// 图片的拷贝
FileInputStream fis = new FileInputStream("C:\\Users\\Ushop\\Desktop\\JavaLoad\\test\\3.png");
FileOutputStream fos = new FileOutputStream("C:\\Users\\Ushop\\Desktop\\JavaLoad\\test\\4.png");
BufferedInputStream bis = new BufferedInputStream(fis);
BufferedOutputStream bos = new BufferedOutputStream(fos);
byte[] b = new byte[1024];
int len = 0;
while((len = bis.read(b)) != -1){
    bos.write(b,0,len);
}
bis.close();
bos.close();
ObjectInputStream

反序列化:在恢复数据时,恢复数据的值和数据类型

class ObjectInputStream_ {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 传入文件不管后缀是什么,都会以特点的文件格式存储
        String filePath = "C:\\Users\\Ushop\\Desktop\\JavaLoad\\test\\hello.txt";
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath));
        // 反序列化的顺序和原文件数据顺序需要一致
        System.out.println( ois.readByte());
        System.out.println( ois.readUTF());
        System.out.println( ois.readFloat());
        // 需要能访问到自己的类,访问不到将会出现异常 ClassNotFoundException
        System.out.println(ois.readObject().toString());
        ois.close();
    }
}

class car implements Serializable {
    String name;
    int age;

    public car(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "name: "+ name + " age: " + age;
    }
}

image-20220316161318072

OutputStream:字节输出流
FileOutputStream

构造方法

image-20220313200622317

image-20220313200544817

写操作

image-20220313195559550

String fileName = "C:\\Users\\Ushop\\Desktop\\JavaLoad\\test\\hello.txt";
// 得到文件输出流对象
FileOutputStream fos = new FileOutputStream(fileName);
// 写操作
// 如果找到文件则进行写操作,否则将创建该文件
fos.write('a');
fos.close();

// 性能优化 写字符串
String fileName = "C:\\Users\\Ushop\\Desktop\\JavaLoad\\test\\hello.txt";
// 得到文件输出流对象
FileOutputStream fos = new FileOutputStream(fileName);
// 写操作
// 如果找到文件则进行写操作,否则将创建该文件
String fileContent = "我又回来了大家!!";
fos.write(fileContent.getBytes());
fos.close();

读写操作

用FileInputStream和FileOutputStream流进行txt文件读写

// 从hello.txt读出内容
String fileName = "C:\\Users\\Ushop\\Desktop\\JavaLoad\\test\\hello.txt";
FileInputStream fis = new FileInputStream(fileName);
int len = 0;
byte[] reader = new byte[1024];
String copyContent = "";
while((len = fis.read(reader)) != -1){
    copyContent += new  String(reader,0,len);
}
// 将读出的内容写入到copyFile.txt
String outFileName = "C:\\Users\\Ushop\\Desktop\\JavaLoad\\test\\copyFile.txt";
FileOutputStream fos = new FileOutputStream(outFileName);
fos.write(copyContent.getBytes());
fis.close();
fos.close();

// 优化代码:边读边写,防止一次性读入过大文件导致内存溢出
String fileName = "C:\\Users\\Ushop\\Desktop\\JavaLoad\\test\\hello.txt";
String outFileName = "C:\\Users\\Ushop\\Desktop\\JavaLoad\\test\\copyFile.txt";
FileInputStream fis = new FileInputStream(fileName);
FileOutputStream fos = new FileOutputStream(outFileName);
int len = 0;
byte[] reader = new byte[9];

while((len = fis.read(reader)) != -1){
    fos.write(new  String(reader,0,len).getBytes());
}
fis.close();
fos.close();

用FileInputStream和FileOutputStream流进行图片文件读写

image-20220314110039594

String fileName = "C:\\Users\\Ushop\\Desktop\\JavaLoad\\test\\1.png";
String outFileName = "C:\\Users\\Ushop\\Desktop\\JavaLoad\\test\\2.png";
FileInputStream fis = new FileInputStream(fileName);
FileOutputStream fos = new FileOutputStream(outFileName);
int len = 0;
byte[] reader = new byte[1024];
 
while((len = fis.read(reader)) != -1){
    fos.write(reader,0,len);
}
fis.close();
fos.close();
BufferedOutputStream

BufferedOutputStream是字节流,实现缓冲的输出流,可以将多个字节写入底层输出流,而不必对每个字节的写入都调用底层

// 拷贝视频
FileInputStream fis = new FileInputStream("C:\\Users\\Ushop\\Desktop\\JavaLoad\\test\\1.avi");
FileOutputStream fos = new FileOutputStream("C:\\Users\\Ushop\\Desktop\\JavaLoad\\test\\2.avi");
BufferedInputStream bis = new BufferedInputStream(fis);
BufferedOutputStream bos = new BufferedOutputStream(fos);

byte[] b = new byte[1024];
int len = 0;
while((len = bis.read(b)) != -1){
    bos.write(b,0,len);
}

bis.close();
bos.close();
ObjectOutputStream

序列化:保存时,保存数据的值和数据类型

// 代码实现
public class ObjectOutputStream_ {
    public static void main(String[] args) throws IOException {
        // 传入文件不管后缀是什么,都会以特点的文件格式存储
        String filePath = "C:\\Users\\Ushop\\Desktop\\JavaLoad\\test\\hello";
        ObjectOutputStream obs = new ObjectOutputStream(new FileOutputStream(filePath));

        obs.writeByte(100);
        obs.writeChars("100");;
        obs.writeFloat(100.0f);
        obs.writeObject(new car("小黄",18));
        obs.close();
    }
}

class car implements Serializable {
    String name;
    int age;

    public car(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return super.toString();
    }
}

image-20220316154453432

注意事项

  • 序列化数据读写顺序需要一致
  • 要求序列化的对象必须实现了Serializable
  • 序列化的类中添加SerialVersionUID,为了提高版本的兼容性
  • 序列化对象时,默认将对象里面所有属性进行序列化,但除了static或transient修饰的成员
  • 序列化对象时,要求对象里面的属性(比如其他对象)必须也要序列化,数据才会被保存
  • 序列化具备可继承性,当父类实现了串行化接口,该父类的所有子类默认实现了序列化
常见其他字节流

image-20220316170957816

printstream : 打印流

默认输出到显示屏,可以通过setOut方法修改显示地址

image-20220317082339399

字符流

节点流和处理流的区别

  • 节点流是底层流,直接跟数据源相接
  • 处理流包装节点流,既可以消除不同节点流的实现差异,也可以提供更方便的方法来完成输入输出
  • 处理流采用了修饰器设计模式,不会直接与数据源相连

image-20220314203847865

(备注:推回输入流和特殊流也属于处理流)

节点流

节点就是可以从一个指定的数据源进行读写数据

FileReader:字符输入流

image-20220314111216221

构造器

image-20220314160733620

// 读操作
String fileName = "C:\\Users\\Ushop\\Desktop\\JavaLoad\\test\\hello.txt";
FileReader fileReader = new FileReader(fileName);
char c = ' ';
// 寻换读取 使用read
while((c = (char)fileReader.read()) != (char)-1 ){
    System.out.print(c);
}

// 优化读取的一次性的量 字符 => 字符数组
String fileName = "C:\\Users\\Ushop\\Desktop\\JavaLoad\\test\\hello.txt";
FileReader fileReader = new FileReader(fileName);
char[] c = new char[1024];
int len = 0;
// 寻换读取 使用read
while((len = (char)fileReader.read(c)) != (char)-1 ){
    System.out.print(new String(c,0,len));
}

// 注意String 和char数组的转换
new String(char[]);
new String(char[],off,len) // 将索引从off开始len个字符的转换成字符串
FileWriter:字符输出流

image-20220314171840321

构造器

image-20220314164040574

注意:FileWriter使用,必须要关闭(close)或者 刷新(flush),否则写入不到指定的文件!

// 写操作 : 输出字符串到文件
String writerFileName = "C:\\Users\\Ushop\\Desktop\\JavaLoad\\test\\copyFile.txt";
String content = "嘿嘿,打我呀!!";
FileWriter fileWriter = new FileWriter(writerFileName);
fileWriter.write(content);
fileWriter.flush();
fileWriter.close();

// 字符输出流为什么要关闭或者刷新才可以真正的写入文件中
// 源码解读:都调用writeBytes()方法来真正的输出
private void writeBytes() throws IOException {
    bb.flip();
    int lim = bb.limit();
    int pos = bb.position();
    assert (pos <= lim);
    int rem = (pos <= lim ? lim - pos : 0);

    if (rem > 0) {
        if (ch != null) {
            if (ch.write(bb) != rem)
                assert false : rem;
        } else {
            // 本质上还是用了字节输出
            out.write(bb.array(), bb.arrayOffset() + pos, rem);
        }
    }
    bb.clear();
}
处理流(包装流)

处理流也叫包装流,是连接在已存在的节点流之上,为程序提供更加强大的读写功能

常见的字符流:BufferedReader 和 BufferedWriter

BufferedReader
// 优化后的fileReader => 处理流
FileReader fileReader = new FileReader("C:\\Users\\Ushop\\Desktop\\JavaLoad\\test\\hello.txt");
BufferedReader br = new BufferedReader(fileReader);
String content = "";
while((content = br.readLine()) != null){
    System.out.print(content);
}
// 注意:关闭流只需要关闭外层流(处理流)就可以了 
br.close();
} 

// 源码解读
public void close() throws IOException {
    synchronized (lock) {
        if (in == null)
            return;
        try {
            in.close(); // in就是节点流FileReader,在底层关闭了
        } finally {
            in = null;
            cb = null;
        }
    }
}
BufferedWriter
String fileName = "C:\\Users\\Ushop\\Desktop\\JavaLoad\\test\\hello.txt";
//  new FileWriter(fileName,true) 后面为true是追加数据,默认false不追加
FileWriter fileWriter = new FileWriter(fileName);
BufferedWriter writer = new BufferedWriter(fileWriter);
// 当前写入都是覆盖而不是追加
writer.write("大家好呀,我是小笙!"); 
writer.newLine(); // 换行符,与操作系统相关的换行操作
writer.write("换行符的使用");
writer.close();
InputStreamReader

Reader的子类:可以将InputStream(字节流)转换成Reader(字符流)

OutputStreamWriter

Writer的子类:可以将OutputStream(字节流)转换成Writer(字符流)

以下用InputStreamReader举例

文件保存编码

image-20220316204213177

// 代码示例
public static void main(String[] args) throws IOException {
    // 解决乱码问题
    String filePath = "C:\\Users\\Ushop\\Desktop\\JavaLoad\\test\\copyFile.txt";
    InputStreamReader isr = new InputStreamReader(new FileInputStream(filePath),"gbk");
    char[] c = new char[1024];
    int len = 0;
    while ((len = isr.read(c)) != -1) {
        System.out.println(new String(c,0,len));
    }
}

正确解码

image-20220316204503707

错误解码

image-20220316204358599

相关面试题

1.介绍一下Java中的IO流

Java提供了大量的类来支持IO操作,下表给大家整理了其中比较常用的一些类。其中,黑色字体的是抽象基类,其他所有的类都继承自它们。红色字体的是节点流,蓝色字体的是处理流。

image-20220317092617813

根据命名很容易理解各个流的作用:

  • 以File开头的文件流用于访问文件;
  • 以ByteArray/CharArray开头的流用于访问内存中的数组;
  • 以Piped开头的管道流用于访问管道,实现进程之间的通信;
  • 以String开头的流用于访问内存中的字符串;
  • 以Buffered开头的缓冲流,用于在读写数据时对数据进行缓存,以减少IO次数;
  • InputStreamReader、InputStreamWriter是转换流,用于将字节流转换为字符流;
  • 以Object开头的流是对象流,用于实现对象的序列化;
  • 以Print开头的流是打印流,用于简化打印操作;
  • 以Pushback开头的流是推回输入流,用于将已读入的数据推回到缓冲区,从而实现再次读取;
  • 以Data开头的流是特殊流,用于读写Java基本类型的数据。

2.介绍一下Java的序列化与反序列化

序列化机制可以将对象转换成字节序列,这些字节序列可以保存在磁盘上,也可以在网络中传输,并允许程序将这些字节序列再次恢复成原来的对象。其中,对象的序列化(Serialize),是指将一个Java对象写入IO流中,对象的反序列化(Deserialize),则是指从IO流中恢复该Java对象。

若对象要支持序列化机制,则它的类需要实现Serializable接口,该接口是一个标记接口,它没有提供任何方法,只是标明该类是可以序列化的,Java的很多类已经实现了Serializable接口,如包装类、String、Date等。

若要实现序列化,则需要使用对象流ObjectInputStream和ObjectOutputStream。其中,在序列化时需要调用ObjectOutputStream对象的writeObject()方法,以输出对象序列。在反序列化时需要调用ObjectInputStream对象的readObject()方法,将对象序列恢复为对象。

3.Serializable接口为什么需要定义serialVersionUID变量?

serialVersionUID代表序列化的版本,通过定义类的序列化版本,在反序列化时,只要对象中所存的版本和当前类的版本一致,就允许做恢复数据的操作,否则将会抛出序列化版本不一致的错误。

如果不定义序列化版本,在反序列化时可能出现冲突的情况,例如:

  1. 创建该类的实例,并将这个实例序列化,保存在磁盘上;
  2. 升级这个类,例如增加、删除、修改这个类的成员变量;
  3. 反序列化该类的实例,即从磁盘上恢复修改之前保存的数据。

在第3步恢复数据的时候,当前的类已经和序列化的数据的格式产生了冲突,可能会发生各种意想不到的问题。增加了序列化版本之后,在这种情况下则可以抛出异常,以提示这种矛盾的存在,提高数据的安全性。

网络编程

网络基础知识

概念
  • 概念:两台或者多台设备之间通过网络实现数据传输
  • Java.net包下提供了一系列的类和接口用来实现网络通信
  • 网络的覆盖范围
    • 局域网:一个机房或者教室
    • 城域网:一个城市
    • 广域网:全国甚至全球范围
IP地址
  • 概念:用来标识每一台电脑主机 <=> 地址
  • 查看ip地址:ipconfig
  • 组成:网络地址+主机地址 如 192.168.16.22

IPv6的地址长度为128位,16个字节是IPv4的四倍

image-20211113143303713

image-20211113145529149

域名

概念:将ip地址映射成域名

目的:解决记忆ip地址困难的问题

www.baidu.com <=> 180.101.49.11

image-20211113150218328

端口号

概念:用于标识特定的网络服务程序(注意是网络服务需要端口,普通程序并不一定需要占用端口)

范围:以整数形式 0~65535(2个字节) (注意0~1024通常被占用

image-20211113152710508

image-20211113151846001

网络通讯协议

数据,在网络编程中,数据的组织形式就是协议(需要按照规定好的协议方式)

image-20211113154345000

image-20220317184548885

TCP 和 UDP

image-20211113170033305

InetAddress

实现功能

image-20211113173703985

方法:(InetAddress对象: 主机名/IP地址)

  • 获取本机的InetAddress对象 — localHost
  • 根据指定主机名获取InetAddress对象 — InetAddress.getByName(“主机名”)
  • 根据域名返回InetAddress对象 — InetAddress.getByName(“域名”)
  • 通过InetAddress对象,获取对应的IP地址 — InetAddress对象.getHostAddress()
  • 通过InetAddress对象 获取对应的主机名或者域名 — InetAddress对象.getByName(“主机名”)

操作代码示例

需要捕获异常,可能会出现异常

比如找不到local host 或者不允许找

image-20211113182725146

// 需要捕获异常,可能会出现异常
try {
    // 1.获取本机的InetAddress对象  --- LAPTOP-EINLAL7G/192.168.56.1
    InetAddress InetObject1 = InetAddress.getLocalHost();
    System.out.println("InetObject1: "+ InetObject1);

    // 2.根据指定主机名获取InetAddress对象  --- LAPTOP-EINLAL7G/192.168.56.1
    InetAddress InetObject2 = InetAddress.getByName("LAPTOP-EINLAL7G");
    System.out.println("InetObject2: "+ InetObject2);

    // 3.根据域名返回InetAddress对象  --- www.taobao.com/60.163.129.165
    InetAddress InetObject3 = InetAddress.getByName("www.taobao.com");
    System.out.println("InetObject3: "+ InetObject3);

    // 4.通过InetAddress对象,获取对应的IP地址  --- 180.101.49.11
    InetAddress InetObject = InetAddress.getByName("www.baidu.com");
    System.out.println(InetObject.getHostAddress());

    // 5.通过InetAddress对象 获取对应的主机名或者域名 --- www.jd.com/60.165.115.3
    InetObject = InetAddress.getByName("www.baidu.com");
    System.out.println(InetObject.getByName("www.jd.com"));

}catch(Exception e){}

Socket

编程方式:1.TCP编程 可靠 2.UDP编程 不可靠

TCP网络通信编程

客户端 <==> 服务端

image-20211114133940621

image-20211114134611442

发送一次数据案例(字节流)

题目要求如下

  1. 编写一个服务端和一个客户端
  2. 服务端在9999端口监听
  3. 客户端连接服务端并发送一串字符串(字节数组) 然后退出
  4. 服务端接收到客户端发送的信息 输出并退出

image-20211114140531301

实现代码

客户端

public class ClientTCP {
    /**
     * 客户端
     * 思路:
     * 1.连接服务端(ip,端口)
  
     */
    public static void main(String[] args) throws IOException {
        //  1.连接服务端(ip,端口)
        Socket socket = new Socket(InetAddress.getLocalHost(),9999);

        // 2.连接上服务端,生成socket 并通过socket.getOutputStream()写入数据
        OutputStream outputStream = socket.getOutputStream();
        // 输入想要传输的字符串进行传输
        System.out.println("请你输入想要传输的字符串");
        String str = new Scanner(System.in).next();
        byte[] out = str.getBytes();
        outputStream.write(out) ;
        System.out.println("传输成功!!");

        // 3.最后要释放流 and socket
        outputStream.close();
        socket.close();
        System.out.println("客户端结束传输");
    }
}

服务端

public class TCPServer {
    /**
     * 服务端
     * 思路:
     * 1.设置监听端口为9999
     * 2.监听客户端,是否有建立连接
     *   如果建立连接则通过socket.getInputStream()接受数据
     *   如果没有建立连接则一直等待直到建立连接
     * 3.最后要释放流 and socket serverSocket
     */
    public static void main(String[] args) throws IOException {
        // 1.设置监听端口为9999
        ServerSocket serverSocket = new ServerSocket(9999);

        // 2.监听客户端,是否有建立连接
        // 如果没有建立连接则一直等待直到建立连接
        System.out.println("开始接收数据....");
        Socket socket = serverSocket.accept();

        // 如果建立连接则通过socket.getInputStream()接受数据
        // 如果没有传入数据则等待数据的传入
        InputStream inputStream = socket.getInputStream();
        byte[] buf = new byte[1024];
        int readLength = 0;
        while((readLength = inputStream.read(buf)) != -1){
            System.out.println("接收到数据:"+new String(buf,0,readLength));
        }

        // 3.最后要释放流 and socket serverSocket
        inputStream.close();
        socket.close();
        serverSocket.close();
        System.out.println("服务端结束传输");
    }
}

实现结果

image-20211114164525198

image-20211114164612412

数据往返传输案例 (字节流)

题目要求如下

  1. 编写一个服务端和一个客户端
  2. 服务端在9999端口监听
  3. 客户端连接服务端并发送一串字符串(字节数组)并且接收到服务端传来的数据并且显示 然后退出
  4. 服务端接受到客户端发送的信息并传送回一串字符串(字节数组) 然后退出

image-20211114171311526

注意点:socket传输完需要添加结束标记 如:socket.shutdownInput(); // 关闭输入流 socket.shutdownOutput(); // 关闭输出流

客户端

public class ClientTCP {
    /**
     * 客户端
     * 思路:
     * 1.连接服务端(ip,端口)
     * 2.连接上服务端,生成socket 并通过socket.getOutputStream()写入数据
     * 3.接收服务器传来的数据并显示
     * 4.最后要释放流 and socket
     */
    public static void main(String[] args) throws IOException {
        //  1.连接服务端(ip,端口)
        Socket socket = new Socket(InetAddress.getLocalHost(),9999);

        // 2.连接上服务端,生成socket 并通过socket.getOutputStream()写入数据
        OutputStream outputStream = socket.getOutputStream();
        // 输入想要传输的字符串进行传输
        System.out.println("请你输入想要传输的字符串");
        String str = new Scanner(System.in).next();
        byte[] out = str.getBytes();
        outputStream.write(out) ; 
        System.out.println("传输成功!!");
        socket.shutdownOutput();  // 关闭输出流

        // 3.接收服务器传来的数据并显示
        InputStream inputStream = socket.getInputStream();
        byte[] buf = new byte[1024];
        int readLength = 0;
        while((readLength = inputStream.read(buf)) != -1){
            System.out.println("接收到数据:"+new String(buf,0,readLength));
        }

        // 4.最后要释放流 and socket
        inputStream.close();
        outputStream.close();
        socket.close();
        System.out.println("客户端结束传输");
    }
}

服务端

public class TCPServer {
    /**
     * 服务端
     * 思路:
     * 1.设置监听端口为9999
     * 2.监听客户端,是否有建立连接
     *   如果建立连接则通过socket.getInputStream()接受数据
     *   如果没有建立连接则一直等待直到建立连接
     * 3.接受到数据并且传送回一串字符串
     * 4.最后要释放 流 and socket and serverSocket
     */
    public static void main(String[] args) throws IOException {
        // 1.设置监听端口为9999
        ServerSocket serverSocket = new ServerSocket(9999);

        // 2.监听客户端,是否有建立连接
        // 如果没有建立连接则一直等待直到建立连接
        System.out.println("开始接收数据....");
        Socket socket = serverSocket.accept();

        // 如果建立连接则通过socket.getInputStream()接受数据
        // 如果没有传入数据则等待数据的传入
        InputStream inputStream = socket.getInputStream();
        byte[] buf = new byte[1024];
        int readLength = 0;
        while((readLength = inputStream.read(buf)) != -1){
            System.out.println("接收到数据:"+new String(buf,0,readLength));
        }
        socket.shutdownInput(); // 关闭输入流

        // 3.接受到数据并且传送回一串字符串
        System.out.println("想要输出撒?");
        OutputStream outputStream = socket.getOutputStream();
        String str = new Scanner(System.in).next();
        outputStream.write(str.getBytes());
        System.out.println("输出成功!!!");


        // 4.最后要释放流 and socket serverSocket
        outputStream.close();
        inputStream.close();
        socket.close();
        serverSocket.close();
        System.out.println("服务端结束传输");
    }
}

实现结果

image-20211114175421041

image-20211114175511636

数据往返传输案例 (字符流)

题目要求如下

  1. 编写一个服务端和一个客户端
  2. 服务端在9999端口监听
  3. 客户端连接服务端并发送一串字符串并且接收到服务端传来的数据并且显示 然后退出
  4. 服务端接受到客户端发送的信息并传送回一串字符串 然后退出

image-20211114214701218

演示核心代码

// 客户端
String str = new Scanner(System.in).next();
//  转换流重点:An OutputStreamWriter is a bridge from character streams to byte streams
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
bufferedWriter.write(str) ;
bufferedWriter.newLine(); // 换行符 表示写入的内容结束  注意对方必须是使用readline()
bufferedWriter.flush();  // 如果使用的是字符流,需要手动去刷新,否则数据将无法写入数据通道 
System.out.println("传输成功!!");

// 服务端
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
System.out.println(bufferedReader.readLine());

// 注意关闭外层流
bufferedWriter.close();
bufferedReader.close();
发送一张图片

题目要求如下

  1. 编写一个服务端和一个客户端
  2. 服务端在8899端口监听
  3. 客户端连接到服务端,发送一张图片 地址:C:\Users\Ushop\Desktop\mess\Java\Al_tair.png
  4. 服务端接受到客户端发送的图片,保存到src下,然后再发送“收到图片”再退出
  5. 客户端接受到“收到图片”再退出

image-20211116173938490

我遇到了一点小问题,暂时还不清楚原因:只能传输.jpg图片,不能传输.png图片

image-20211118200241060

代码如下

客户端

public class TCPFileuploadClient {
    /**
     * 客户端
     * 1. 编写一个服务端和一个客户端
     * 2. 服务端在8899端口监听
     * 3. 客户端连接到服务端,发送一张图片 地址:C:\Users\Ushop\Desktop\mess\Java\Al_tair.png
     * 4. 服务端接受到客户端发送的图片,保存到src下,然后再发送“收到图片”再退出
     * 5. 客户端接受到“收到图片”再退出
     */
    public static void main(String[] args) throws Exception {
        // 1. 连接到服务端8899端口
        Socket socket = new Socket(InetAddress.getLocalHost(), 9999);

        // 2.创建读取磁盘文件放入输入流
        String filePath = "C:\\Users\\Ushop\\Desktop\\JavaLoad\\JavaSe\\Image\\pandas.jpg";
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filePath));

        // 3.通过socket获取到输入流,将byte数据发送到服务端
        byte[] bytes = StreamUtils.streamToByteArray(bis);
        BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
        bos.write(bytes);
        socket.shutdownOutput(); // 结束输出流

        // 4.接受服务端发来的“收到图片”

        BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        System.out.println(reader.readLine());

        // 5.关闭相关资源
        reader.close();
        bis.close(); 
        bos.close();
        socket.close();
    }
}

服务端

public class TCPFileuploadServer {
    /**
     * 服务端
     * 1. 编写一个服务端和一个客户端
     * 2. 服务端在8899端口监听
     * 3. 客户端连接到服务端,发送一张图片 地址:C:\Users\Ushop\Desktop\mess\Java\Al_tair.png
     * 4. 服务端接受到客户端发送的图片,保存到src下,然后再发送“收到图片”再退出
     * 5. 客户端接受到“收到图片” 再退出
     */
    public static void main(String[] args) throws Exception {
        // 1.服务端在本地监听8899端口
        ServerSocket serverSocket = new ServerSocket(9999);

        // 2.服务端等待连接
        System.out.println("等待客户端发送数据.....");
        Socket socket = serverSocket.accept();

        // 3.读取客户端发送的数据 通过Socket得待输入流
        BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
        byte[] bytes = StreamUtils.streamToByteArray(bis);

        // 4.将得到的byte数组,写入到指定的路径,就得到一个文件了
        String descFilePath = "E:\\Java_training\\Java_code\\JavaIdea03\\java\\Javase_HSping\\src\\com\\Al_tair\\socket\\upload\\pandas.jpg";
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(descFilePath));
        bos.write(bytes);


        // 5.向客户端发送“收到图片”
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        writer.write("收到图片");
        writer.newLine();
        writer.flush();

        // 6.最后关闭其他资源
        bis.close();
        bos.close();
        writer.close();
        socket.close();
        serverSocket.close();

    }
}

工具类 :将.转换成byte[]字节数组

public class StreamUtils {
    /**
     * 功能:将输入流转换成byte[]字节数组
     */
    public static byte[] streamToByteArray(InputStream inputstream) throws Exception{
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); // 创建输出流对象
        byte[] b = new byte[1024]; // 字节数组
        int len;
        while((len = inputstream.read(b))!= -1){
            byteArrayOutputStream.write(b,0,len);
        }
        byte[] array = byteArrayOutputStream.toByteArray(); // 将输出流转换成字节数组
        byteArrayOutputStream.close();
        return array;
    }
}
netstat 指令(补充)
  1. netstat -an 可以查看当前主机网络情况,包括端口监听和网络连接情况

  2. netstat -an | more 可以分页显示 ctrl+c退出该指令

    image-20211119114947310

  3. dos控制行以管理员身份打开 netstat -anb 可以查看哪个应用软件监听该端口

  4. 当客户端连接到服务端后实际上客户 TCP/IP随机分配的端口(验证:netstat观测)

    image-20211119122203549

UDP网络通信编程[了解]

基本介绍

  • 类DatagramSocket 和 DatagramPacke[数据包/数据报] 实现了基于UDP 协议网络程序
  • UDP数据报套接字DatagramSocket 发送和接受数据,系统不保证UDP数据报一定能安全送达目的地也不能确定什么时候可以送到
  • DatagramPacket 对象封装了UDP数据报,在数据报中包含了发送端的IP 地址和端口号以及接收端的IP地址和端口号
  • UDP协议中每个数据报都给出了完整的地址信息. 因此无需建立连接

image-20211119172349275

UDP网络通信编程案例

任务要求:

  1. 编写一个接受端A和一个发送端B
  2. 接受端A 在9999端口等待接收数据报
  3. 发送端B向接收端A发送数据“hello~”
  4. 接收端A接收到发送端B发送的数据回复“hello , Im fine”,再退出
  5. 发送端B接收到回复的数据再退出

但是注意程序启动顺序还是有区别的,接收方要先启动程序!!!

public class udpNodeB {
    /**
     * Node B
     * 1. 编写一个接受端A和一个发送端B
     * 2. 接受端A 在9999端口等待接收数据报
     * 3. 发送端A向接收端B发送数据“hello~”
     * 4. 接收端B接收到发送端A发送的数据回复“hello , Im fine”,再退出
     * 5. 发送端A接收到回复的数据再退出
     */
    public static void main(String[] args) throws IOException {
        // 1.创建一个DatagramSocket对象,准备在9998端口接收数据
        DatagramSocket socket = new DatagramSocket(9998);

        // 2.将需要发送的数据封装到 DatagramPacket对象 (UDP协议 数据包MAX 64K)
        System.out.println("开始聊天界面...");
        String str = new Scanner(System.in).next();
        byte[] data = str.getBytes();

        // 3.封装的 DatagramPacket对象组成: data 数据 | data.length 数据长度 | 主机(IP) | 端口(port)
        // send 方法发送数据
        DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName("192.168.56.1"), 9999);
        socket.send(packet);
        System.out.println("数据发送成功!!");
        
        // 复制接受端A的j数据的方式
        byte[] buf = new byte[64*1024];
        packet = new DatagramPacket(buf,buf.length);
        socket.receive(packet);
        String s = new String(packet.getData(), 0, packet.getLength());
        System.out.println(s);

        // 5.释放资源
        socket.close();

    }
}
public class udpNodeA {
    /**
     * Node A
     * 1. 编写一个接受端A和一个发送端B
     * 2. 接受端A 在9999端口等待接收数据报
     * 3. 发送端B向接收端A发送数据“hello~”
     * 4. 接收端A接收到发送端B发送的数据回复“hello , Im fine”,再退出
     * 5. 发送端B接收到回复的数据再退出
     */
    public static void main(String[] args) throws IOException {
        // 1.创建一个DatagramSocket对象,准备在9999端口接收数据
        DatagramSocket socket = new DatagramSocket(9999);

        // 2.创建一个 DatagramPacket对象,准备接收数据 (UDP协议 数据包MAX 64K)
        byte[] buf = new byte[64*1024];
        DatagramPacket packet = new DatagramPacket(buf,buf.length);

        // 3.调用接收方法,将通过网络传输的DatagramPacket对象填充到packet对象
        // receive方法会在9999端口一直等待直到接收到数据
        System.out.println("开始接受数据...");
        socket.receive(packet);

        // 4.可以把packet进行拆包,取出数据并显示
        String s = new String(packet.getData(), 0, packet.getLength());
        System.out.println(s);
        System.out.println("数据接收成功...");
        
        // 复制发送端B的发送数据的方式
        String str = new Scanner(System.in).next();
        byte[] data = str.getBytes();
        packet = new DatagramPacket(data, data.length, InetAddress.getByName("192.168.56.1"), 9998);
        socket.send(packet);

        // 5.释放资源
        socket.close();
    }
}

相关面试题

1.讲一下TCP/IP协议

  1. TCP/IP协议定义

    TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是指能够在多个不同网络间实现信息传输的协议簇。TCP/IP协议不仅仅指的是TCP和IP两个协议,而是指一个由FTP、SMTP、TCP、UDP、IP等协议构成的协议簇, 只是因为在TCP/IP协议中TCP协议和IP协议最具代表性,所以被称为TCP/IP协议。

  2. TCP/IP协议组成

    TCP/IP结构模型分为**应用层、传输层、网络层、链路层(网络接口层)**四层,以下是各层的详细介绍:

    (1)应用层

    应用层是TCP/IP协议的第一层,是直接为应用进程提供服务的。

    a. 对不同种类的应用程序它们会根据自己的需要来使用应用层的不同协议,邮件传输应用使用了SMTP协议、万维网应用使用了HTTP协议、远程登录服务应用使用了有TELNET协议。

    b. 应用层还能加密、解密、格式化数据。

    c. 应用层可以建立或解除与其他节点的联系,这样可以充分节省网络资源。

    (2)传输层

    作为TCP/IP协议的第二层,运输层在整个TCP/IP协议中起到了中流砥柱的作用。且在运输层中,TCP和UDP也同样起到了中流砥柱的作用。

    (3)网络层

    网络层在TCP/IP协议中的位于第三层。在TCP/IP协议中网络层可以进行网络连接的建立和终止以及IP地址的寻找等功能。

    (4)链路层(网络接口层)

    在TCP/IP协议中,网络接口层位于第四层。由于网络接口层兼并了物理层和数据链路层。所以,网络接口层既是传输数据的物理媒介,也可以为网络层提供一条准确无误的线路。

  3. TCP/IP协议特点

    TCP/IP协议能够迅速发展起来并成为事实上的标准,是它恰好适应了世界范围内数据通信的需要。它有以下特点:

    (1)协议标准是完全开放的,可以供用户免费使用,并且独立于特定的计算机硬件与操作系统;

    (2)独立于网络硬件系统,可以运行在广域网,更适合于互联网;

    (3)网络地址统一分配,网络中每一设备和终端都具有一个唯一地址;

    (4)高层协议标准化,可以提供多种多样可靠网络服务。

    2.介绍一下tcp的三次握手

    1. 第一次握手:建立连接时,客户端发送syn包(syn=x)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
    2. 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;
    3. 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

    image-20220319172024779

3.介绍一下TCP和UDP的区别

TCP和UDP有如下区别:

  1. 连接:TCP面向连接的传输层协议,即传输数据之前必须先建立好连接;UDP无连接。
  2. 服务对象:TCP点对点的两点间服务,即一条TCP连接只能有两个端点;UDP支持一对一,一对多,多对一,多对多的交互通信。
  3. 可靠性:TCP可靠交付:无差错,不丢失,不重复,按序到达;UDP尽最大努力交付,不保证可靠交付。
  4. 拥塞控制/流量控制:有拥塞控制和流量控制保证数据传输的安全性;UDP没有拥塞控制,网络拥塞不会影响源主机的发送效率。
  5. 报文长度:TCP动态报文长度,即TCP报文长度是根据接收方的窗口大小和当前网络拥塞情况决定的;UDP面向报文,不合并,不拆分,保留上面传下来报文的边界。
  6. 首部开销:TCP首部开销大,首部20个字节;UDP首部开销小,8字节(源端口,目的端口,数据长度,校验和)。
  7. 适用场景(由特性决定):数据完整性需让位于通信实时性,则应该选用TCP 协议(如文件传输、重要状态的更新等);反之,则使用 UDP 协议(如视频传输、实时通信等)。

反射

反射机制

什么是反射?

反射就是Reflection,Java的反射是指程序在运行期可以拿到一个对象的所有信息。这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制

反射就是把java类中的各种成分(成员变量,方法,构造器等等类的成员)映射成一个个的Java对象

反射的优缺点

优点:可以动态的创建和使用对象,使用灵活(框架底层核心)

缺点:使用反射基本是解释执行,对执行的效率有很大的影响

class类

Class类的实例表示java应用运行时的类或接口(每个java类运行时都在JVM里表现为一个class对象,可通过类名.class、类型.getClass()、Class.forName(“类名”)等方法获取class对象

// 总结
// 1.注意Class类和class关键字的区别
// 2.私有构造函数,只有 Java 虚拟机会创建 Class 对象
// 3.每个通过关键字class标识的类,在内存中有且只有一个与之对应的Class对象来描述其类型信息,无论创建多少个实例对象,其依据的都是用一个Class对象
public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement,
                              TypeDescriptor.OfField<Class<?>>,
                              Constable {
    private static final int ANNOTATION= 0x00002000;
    private static final int ENUM      = 0x00004000;
    private static final int SYNTHETIC = 0x00001000;

    private static final ClassDesc[] EMPTY_CLASS_DESC_ARRAY = new ClassDesc[0];

    private static native void registerNatives();
    static {
        registerNatives();
    }

    /*
     * Private constructor. Only the Java Virtual Machine creates Class objects.
     * This constructor is not used and prevents the default constructor being
     * generated.
     * 私有构造函数,只有 Java 虚拟机会创建 Class 对象。不使用此构造函数并阻止生成默认构造函数。
     */
    private Class(ClassLoader loader, Class<?> arrayComponentType) {
        // Initialize final field for classLoader.  The initialization value of non-null
        // prevents future JIT optimizations from assuming this final field is null.
        classLoader = loader;
        componentType = arrayComponentType;
    }

类加载阶段

// 代码示例
class Cat{
	private String name;
	public Cat(){}
	public void hi(){}
}

image-20220320195240128

类加载的生命周期

类加载的过程包括了加载验证准备解析初始化五个阶段

注意:加载验证准备初始化四个阶段的开始顺序是依次如此,但是在运行中会交叉运行程序(如:相互调用);解析阶段可能会出现在初始化之后(为了支持动态绑定)

image-20220323143344970

类的加载示例(图片

image-20220323143659380

反射的使用

Class类的对象获取
  • 根据类名:类名.class
  • 根据对象:对象.getClass()
  • 根据全限定类名:Class.forName(全限定类名)
public class Reflection01 {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        // 测试用例
        // 1.forName()
        // (1)获取Class对象的一个引用,但引用的类还没有加载(该类的第一个对象没有生成)就加载了这个类。
        // (2)为了产生Class引用,forName()立即就进行了初始化。
        System.out.println("forName: "+Class.forName("com.Al_tair.reflection.Cat")); // forName: class com.Al_tair.reflection.Cat
        
        // 2.Object-getClass() 获取Class对象的一个引用,返回表示该对象的实际类型的Class引用。
        System.out.println("getClass: "+new Cat().getClass()); // getClass: class com.Al_tair.reflection.Cat
        
        // 3.getName() 取全限定的类名(包括包名),即类的完整名字。
        System.out.println("getName: "+com.Al_tair.reflection.Cat.class); // getName: class com.Al_tair.reflection.Cat
        
        // 4.getSimpleName() 获取类名(不包括包名)
        System.out.println("getSimpleName: "+ new Cat().getClass().getSimpleName()); // getSimpleName: Cat
        
        // 5.getCanonicalName() 获取全限定的类名(包括包名)
        System.out.println("getCanonicalName: "+ new Cat().getClass().getCanonicalName()); // getCanonicalName: com.Al_tair.reflection.Cat
        
        // 6.isInterface() 判断Class对象是否是表示一个接口
        System.out.println("isInterface: "+ new Cat().getClass().isInterface()); // isInterface: false
        System.out.println("isInterface: "+ Fly.class.isInterface()); // isInterface: true
        
        // 7.getInterfaces() 返回Class对象数组,表示Class对象所引用的类所实现的所有接口。
        for (Class list:new Cat().getClass().getInterfaces()){
            System.out.println(list); // interface com.Al_tair.reflection.Fly
        }
       
        // 8.getSuperclass() 返回Class对象,表示Class对象所引用的类所继承的直接基类。应用该方法可在运行时发现一个对象完整的继承结构。
        System.out.println("getSuperclass"+new Cat().getClass().getSuperclass()); // getSuperclassclass com.Al_tair.reflection.Animal
        
        // 9. getFields() 获得某个类的所有的公共(public)的字段,包括继承自父类的所有公共字段。 类似的还有getMethods和getConstructors。
        //    getDeclaredFields 获得某个类的自己声明的字段,即包括public、private和proteced,默认但是不包括父类声明的任何字段
        Cat  cat = Cat.class.newInstance();
        for (Field f: cat.getClass().getFields()) {
            System.out.print(f.getName()+" "); // name color age 
        }
    }
}
interface Fly{
}
class Animal{
    public int age;
}
class Cat extends Animal implements Fly {
    public String name = "";
    public String color;

    public Cat() {
    }

    public Cat(String name, String color, int age) {
        this.name = name;
        this.color = color;
        this.age = age;
    }

    @Override
    public String toString() {
        return "cat{" +
                "name='" + name + '\'' +
                ", color='" + color + '\'' +
                ", age=" + age +
                '}';
    }
}
Constructor类及其用法

Class类与Constructor相关的主要方法如下:

image-20220323172016983

关于Constructor类本身一些常用方法如下(仅部分,其他可查API)

image-20220323172033657

我在这里就不大篇幅的讲述Constructor类的大量方法,用代码举例一些常用方法如下

public class Constructor01 {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        Class cls = new Person().getClass();
        Person p = (Person)cls.newInstance(); // 调用无参构造器
        System.out.println(p); // User{age=18, name='lns'}

        // 调用public修饰的有参构造器
        Constructor con = cls.getConstructor(String.class);
        Person p2 = (Person)con.newInstance("zlr");
        System.out.println(p2); // User{age=18, name='zlr'}

        // 调用非public修饰的有参构造器
        Constructor decCon = cls.getDeclaredConstructor(int.class,String.class);
        decCon.setAccessible(true); // 设置爆破,突破访问权限
        Person p3 = (Person)decCon.newInstance(12,"zlr"); 
        System.out.println(p3); // User{age=12, name='zlr'}
    }
}

class Person {
    private int age = 18;
    private String name = "lns";
    public Person() {
        super();
    }
    public Person(String name) {
        super();
        this.name = name;
    }

    private Person(int age, String name) {
        super();
        this.age = age;
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}
Field类及其用法

Class类与Field对象相关方法如下:

image-20220323172124461

关于Field类还有其他常用的方法如下:

image-20220323172204939

我在这里就不大篇幅的讲述Field类的大量方法,用代码举例一些常用方法如下

public class Field01 {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchFieldException {
        Class<?> cls = Class.forName("com.Al_tair.reflection.Student");
        Student s = (Student)cls.newInstance();
        Field age = cls.getField("age");
        // 设置age属性值(public修饰)
        age.set(s,12);
        System.out.println(s.toString()); // Student{age=12, name='lns'}

        Field name = cls.getDeclaredField("name");
        name.setAccessible(true); // 爆破私有属性
        // 设置name属性(私有属性)
        name.set(s,"zlr");
        System.out.println(s.toString()); // Student{age=12, name='zlr'}

    }
}

class Student {
    public int age = 18;
    private String name = "lns";

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}
Method类及其用法

Class类获取Method对象相关的方法:

image-20220323203449786

常用方法如下:

image-20220323203525186

我在这里就不大篇幅的讲述Method类的大量方法,用代码举例一些常用方法如下

public class Method01 {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        // method.setAccessible(true) 私有方法爆破
        // 访问: method.invoke(object,实参列表) 注意:静态方法的话object对象可以填写成null
        Class<?> cls = Class.forName("com.Al_tair.reflection.car");
        car c = (car)cls.newInstance();
        System.out.println(c.toString()); // car{OilPer=0.6, name='玛莎拉蒂'}
        // 获取的方法的名字和形参类型
        Method name = cls.getMethod("setName", String.class);
        // 传入形参(该方法是公有方法)
        name.invoke(c, "红旗");
        System.out.println(c.toString()); // car{OilPer=0.6, name='红旗'}

        //  getDeclaredMethod 可以是公有的方法也可以是私有的方法
        Method addOil = cls.getDeclaredMethod("addOil", double.class);
        // 私有方法爆破
        addOil.setAccessible(true);
        // 传入形参(该方法是公有方法)
        addOil.invoke(c,1.0);
        System.out.println(c.toString()); // car{OilPer=1.0, name='红旗'}


    }
}

class car{
    private double OilPer = 0.6; // 油量60%
    private String name = "玛莎拉蒂";

    private void addOil(double oilPer){
        this.OilPer = oilPer;
    }

    public void setName(String name){
        this.name = name;
    }

    @Override
    public String toString() {
        return "car{" +
                "OilPer=" + OilPer +
                ", name='" + name + '\'' +
                '}';
    }
}

相关面试题

1. Java反射在实际项目中有哪些应用场景?

  • 使用JDBC时,如果要创建数据库的连接,则需要先通过反射机制加载数据库的驱动程序;
  • 多数框架都支持注解/XML配置,从配置中解析出来的类是字符串,需要利用反射机制实例化;
  • 面向切面编程(AOP)的实现方案,是在程序运行时创建目标对象的代理类,这必须由反射机制来实现。

设计模式(23种)

单例设计模式

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实力的方法

种类:饿汉式 和 懒汉式

public class SingleTon {
    /*
     * 饿汉式(类加载了对象就创建了) 
     * 步骤如下:
     *  1.构造器私有化 => 防止new对象
     *  2.类的内部创建对象(该对象是static)
     *  3.提供一个静态的公共方法 getInstance
     *
     * 懒汉式(只有当使用该方法时才创建对象)
     * 步骤如下:
     *  1.构造器私有化 => 防止new对象
     *  2.定义一个私有的static静态属性对象
     *  3.提供一个公共的static方法,返回Dog对象
     *
     * 总结 饿汉式和懒汉式的区别
     * 1.创建对象的时机不同:饿汉式是在类加载就创建了对象的实例,而懒汉式是在使用时才创建
     * 2.饿汉式不存在线程安全的问题,懒汉式存在线程安全的问题
     * 3.饿汉式存在浪费资源的可能,懒汉式并不存在这种问题
     *
     *  Runtime函数用到了饿汉式 源代码如下
     *  private static final Runtime currentRuntime = new Runtime();
     *  private static Runtime.Version version;
     *  public static Runtime getRuntime() {
     *     return currentRuntime;
     *  }
     *  // Don't let anyone else instantiate this class
     *  private Runtime() {}
     */
    public static void main(String[] args) {
        // 用来区别饿汉式和懒汉式的核心区别
        GirlFriend.age = 18;
        Dog.age = 3; 

//        GirlFriend girlFriend = GirlFriend.getInstance();
//        System.out.println(girlFriend);
//
//        Dog dog = Dog.getInstance();
//        System.out.println(dog.toString());
    }
}

// just one girlfriend
class GirlFriend{
    private String name;

    public  static int age;

    // 构造器私有化
    private GirlFriend(String name){
        System.out.println("调用了GirlFriend的构造器");
        this.name = name;
    }

    // 类的内部创建对象
    private static GirlFriend zsr = new GirlFriend("张申蕊");

    @Override
    public String toString() {
        return "My girlfriend is " + name;
    }
}

// just one dog
class Dog{
    private String name;

    public  static int age;

    // 构造器私有化
    private Dog(String name){
        System.out.println("调用了Dog的构造器");
        this.name = name;
    }

    // 定义一个私有的static静态属性对象
    private static Dog dog;

    // 提供一个公共的static方法,返回Dog对象
    public static Dog getInstance(){
        if(dog == null){
            dog = new Dog("小黄");
        }
        return dog;
    }

    @Override
    public String toString() {
        return "My dog is " + name;
    }

}

模板设计模式

package design_patterns.Template;
public class TemplateDesign {
    /**
     * 模板设计模式(抽象类)
     * 设计一个抽象类如下:
     * 1)编写方法calculateTime() 用于计算某段代码的耗时时间
     * 2)编写抽象方法job()
     * 3)编写一个子类sub,继承抽象类Template,并实现job方法
     * 4)编写一个测试类TestTemplate,看看是否好用
     */
    public static void main(String[] args) {
        A a = new A();
        a.calculateTime();

        B b = new B();
        b.calculateTime();
    }
}
// 抽象模板类 
abstract class Template{ // 抽象类
    public abstract void job();  // 抽象方法

    public void calculateTime(){
        // 得到开始的时间
        long start = System.currentTimeMillis();
        job();
        // 得到开始的时间
        long end = System.currentTimeMillis();
        System.out.println("执行时间 " + (end-start));
    }
}
 
class A extends Template{
    @Override
    public void job() {
        // 计算 1+2+3+...+2000000
        long sum = 0;
        for (long i = 1; i <= 2000000; i++) {
            sum += i;
        }
    }
}

class B extends Template{
    @Override
    public void job() {
        // 计算 1*2*3*...*2000000
        long sum = 1;
        for (long i = 1; i <= 2000000; i++) {
            sum *= i;
        }
    }
}

代理模式

静态代理
// 线程代理类,模拟一个简易的Thread类
public class ThreadProxy implements Runnable{
    private Runnable target = null;

    @Override
    public void run() {
        if(target != null){
            target.run();
        }
    }

    public ThreadProxy() {
    }

    public ThreadProxy(Runnable target) {
        this.target = target;
    }

    public void start(){
        start0();
    }

    private void start0(){
        run();
    }
}

class test implements Runnable{
    public static void main(String[] args) {
        test test = new test();
        ThreadProxy tp = new ThreadProxy(test);
        tp.start();

    }

    @Override
    public void run() {
        System.out.println("开启线程"); // 开启线程
    }
}

修饰器设计模式

// 基抽象类
public abstract class Reader_ {
    public  void readFile(){}
    public  void readString(){}
}
// 节点流模型
class FileReader_ extends Reader_{

    @Override
    public void readFile() {
        System.out.println("读取文件...");
    }
}
// 节点流模型
class StringReader_ extends Reader_{
    @Override
    public void readString() {
        System.out.println("读取字符串...");
    }
}

// 处理流模型
class BufferReader_ extends Reader_{
    private Reader_ in;

    public BufferReader_(Reader_ in) {
        this.in = in;
    }

    public void ReadFiles(int num){
        for (int i = 0; i < num; i++) {
            in.readFile();
        }
    }
}

class test{
    public static void main(String[] args) {
        BufferReader_ bufferReader_ = new BufferReader_(new FileReader_());
        bufferReader_.ReadFiles(3);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

罗念笙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值