Java从入门到放弃

目录

一. 常用的 DOS 命令: 

二. Java 开发环境搭建:

什么是JDK、JRE:

JDK8的下载网址:官网地址

安装JDK8:

 配置path 环境变量:

 第一种方法:

第二种方法:

安装JDK17: 

三. IDEA的使用

1. 系统设置

2. 工程与模块管理 

3. 快捷键的使用 

第1组:通用型

第2组:提高编写速度(上)

第3组:提高编写速度(下)

第4组:类结构、查找和查看源码

第5组:查找、替换与关闭

第6组:调整格式

第7组:Debug快捷键

4. IDEA 常用插件

四. Java基础语法 

1.Java 程序的入口

2. 两种常见的输出语句:

3. 源文件名与类名 

4. 注释

5. 关键字

6.  标识符

7.  变量

(1).变量的概念:

(2). 基本数据类型

计算机存储单位

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

浮点类型:float、double 

字符类型:char 

布尔类型:boolean

 (3)基本数据类型的运算规则

 自动类型提升

 强制类型转换

 (4)字符串类型

8. 运算符

(1)算数运算符 

(2)赋值运算符

(3)比较(关系)运算符

(4)逻辑运算符

(5)位运算符(难点、非重点) 

(6)条件运算符

(7)运算符优先级

 9. 流程控制语句

(1)if-else 条件判断结构

使用Scanner类从键盘获取数据获取随机数

(2)switch-case 选择结构

(3)for 循环

(4)while循环和do-while循环

10. 数组 

(1)数组概念:

(2)一维数组:

(3)二维数组:

(4)Arrays 工具类的使用:

(5)数组中的常见异常:

11. 面向对象

(1)面向对象编程概述

(2)类的实例化与对象的内存解析

(3)成员变量与局部变量的对比

(4)方法的作用与方法的声明:

(5)对象数组及内存解析

(6)方法的重载

(7)可变个数的形参 

(8)方法的参数传递机制 

(9)递归(recursion)方法

(10)关键字:package、import的使用

(11) 面向对象特征一:封装性(encapsulation)

(12)类的成员之三:构造器(Constructor)

(13) 类中属性赋值过程

(14)JavaBean和UML 类图

(15)关键字:this

(16)面向对象的继承性

(17)方法的重写 

(18)关键字:super 

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

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

(21) Object 类的概念

(22)关键字:static 

(23) 单例设计模式与main的理解

(24)类的成员之四:代码块 

(25)类中属性赋值的位置及过程

(26)final关键字 

(27)抽象类与抽象方法(或abstract关键字)

(28)接口(interface)

(29)类的成员之五:内部类

(30)枚举类

(31) Annotation注解、单元测试的使用

(32)包装类 

 12. 异常处理

(1)异常的概述与常见异常的举例

(2)异常处理方式 

(3)自定义异常类 :

13. 多线程 

(1).程序、进程、线程与并行、并发的概念:

(2) 线程的创建方式:

(3)Thread类的常用方法和生命周期:

(4)线程安全问题及解决 

(5)线程安全的懒汉式与死锁

(6)Lock锁

(7) 线程间的通信机制

(8) 创建线程的方式3,4:实现Callable和使用线程池

14. 常用类与基础API

(1)String的理解与不可变性

(2)String的实例化与连接操作 

(3)Spring的构造器和常用方法:

(4)StringBuilder、StringBuffer源码分析与常用方法

(5)JDK8之前日期时间API的使用: Date、 SimpleDateFormat,、 Calendar 

(6)JDK8中新的日期时间API的使用

(7) 使用Comparable接口实现自然排序

(8)其他常用类的使用 

15. 集合框架 

(1)数组的特点、弊端与集合框架体系介绍

(2)Collection接口中的方法使用

(3)List接口的常用方法:

(4)Set不同实现类的对比及Set无序性、不可重复性的剖析 :

(5) Map不同实现类的对比与HashMap中元素的特点

(6)Map接口常用方法:

(7)Treemap和Properties的使用

(8)Collections工具类的使用

16. 泛型 

(1)泛型的理解及其在集合、比较器中的使用

(2)自定义泛型类和方法:

(3)通配符的使用:

17. 数据结构与集合源码

(1) 数据结构剖析

(2) 常用的数据结构

一维数组:

链表:

栈:

队列:

树与二叉树:

(3)ArrayList在JDK7和JDK8中的源码解析

(4)Vector和LinkedList在JDK8中的源码解析

(5)HashMap在JDK7和JDK8中的源码解析

(6)LinkedHashMap与HashSet与LinkedHashSet的源码解析

18. File类与IO流

(1)File类的实例化与常用方法

(2)IO流概述与流的分类

(3)使用FileReader和FileWriter读取、写出文本数据

(4) FilelnputStream \ FileOutputStream的使用

(5)缓冲流的使用

(6)转换流以及各种字符集的使用:

(7)对象流的使用及对象的序列化机制

(8)其他流的使用

 19. 网络编程

(1)网络编程三要素与InetAddress类的使用

(2)网络编程API

(3)TCP与UDP协议剖析 

(4)UDP、URL网络编程

20. 反射机制 

(1)反射、Class的理解与获取Class实例的方式

(2)类的加载过程与类加载器的理解

类的加载过程:

类加载器(classloader)

21. JDK8-17新特性

(1) 新特性的概述

(2)lambda表达式的使用与函数式接口的理解 

(3) 方法引用与构造器引用

(4) StreamAPl使用三环节:实例化、中间操作、终止操作

(5)JDK8之后的语法新特性汇总及API层面变化 


一. 常用的 DOS 命令: 

DOS(Disk Operating System,磁盘操作系统)是 Microsoft 公司在 Windows 之前推出的一个操作系统,是单用户、单任务(即只能执行一个任务)的操作系统。现在被 Windows 系统取代。    

对于 Java 初学者,学习一些 DOS 命令,会非常有帮助。

进入 DOS 操作窗口:

按下 Windows+R 键盘,打开运行窗口,输入 cmd 回车,进入到 DOS 的操作窗口。

常用指令:

dir列出当前目录下的文件以及文件夹
cd 目录进入指定单级目录。
cd 目录 1\目录 2\...进入指定多级目录。cd atguigu\JavaSE
cd \ 或 cd /回退到盘符目录。
cd ..回退到上一级目录。
md 文件目录名创建指定的文件目录。
rd 文件目录名 删除指定的文件目录(如文件目录内有数据,删除失败)
cls清屏。
exit退出命令提示符窗口。
← →移动光标
↑ ↓调阅历史操作命令
Delete 和Backspace删除字符

二. Java 开发环境搭建:

什么是JDK、JRE:

JDK (Java Development Kit):是 Java 程序开发工具包,包含JRE 和开发人员使用的工具。

JRE (Java Runtime Environment) :是 Java 程序的运行时环境,包含JVM 和运行时所  需要的核心类库。

小结: JDK = JRE + 开发工具集(例如Javac编译工具等) JRE = JVM + Java SE标准类库

JDK8的下载网址:官网地址

进入官网后往下翻,找到JAVA8,然后点击Windows,然后选择对应的版本(X64代表32位系统的,X86代表64位系统)。然后跟着提示进行登录,接着会自动进行下载文件

安装JDK8:

点击下载下来的安装包按着提示进行安装,要创建jdk和jre两个文件夹,第一个需要更改路径安装的是jdk第二个是jre

 配置path 环境变量:

什么是path环境变量: 

   window操作系统执行命令时,所要搜寻的路径。

为什么配置path:

   希望在命令行使用javac.exe等工具时,任意目录下都可以找到这个工具所在的目录。

 第一种方法:

打开桌面上的计算机,进入后在左侧找到此电脑—>单击鼠标右键选择属性—>打开高级系统设置—>打开环境变量

 在系统变量中,选中Path 环境变量,双击或者点击编辑—>点击新建并将jdk的路径\bin的路径写入—>并将它上移到最上方,如图所示:

第二种方法:

 在系统变量中,单击新建 ,创建新的环境变量,变量名输入JAVA_HOME,变量值输入jdk的路径

选中Path 环境变量,双击或者点击编辑,在变量值的最前面,键入%JAVA_HOME%\bin。

 注意:强烈建议将%JAVA_HOME%\bin声明在path环境变量中所有变量的最前面!

环境变量配置完成,重启DOS命令行,在任意目录下输入javac 或java 命令或java -version,运行成功。

安装JDK17: 

 JDK17在安装之后,自动进行了环境变量的配置。如下:

想使用哪个版本的jsk就把哪个版本放在最上面 

三. IDEA的使用

1. 系统设置

(1) 下载中文插件

(2)设置用鼠标滚轮设置字体大小

(3) 代码智能提示功能

代码提示和补充功能有一个特性:区分大小写。 如果想不区分大小写的话,就把这个对勾去掉。建议去掉勾选。 

 (4)自动导包配置

(5)设置项目文件编码(一定要改)

(6) 设置控制台的字符编码

(7)设置自动编译

(8)取消双击shift搜索 

(9)修改类头的文档注释信息

2. 工程与模块管理 

IDEA 项目结构:

层级关系: project(工程) - module(模块) - package(包) - class(类)

具体的: 一个project中可以创建多个module

               一个module中可以创建多个package

               一个package中可以创建多个class

               这些结构的划分,是为了方便管理功能代码。 

Project 和 Module 的概念:

在 IntelliJ IDEA 中,提出了 Project 和 Module 这两个概念。

在 IntelliJ IDEA 中 Project 是最顶级的结构单元,然后就是Module。目前,主流的大型项目结构基本都是多Module的结构,这类项目一般是按功能划分的,比如:user-core-module、user-facade-module 和 user-hessian-module 等等, 模块之间彼此可以相互依赖,有着不可分割的业务关系。因此,对于一个 Project 来说:

当为单Module项目的时候,这个单独的Module实际上就是一个Project。

当为多Module项目的时候,多个模块处于同一个Project之中,此时彼此之间具有互相依赖的关联关系。

当然多个模块没有建立依赖关系的话,也可以作为单独一个“小项目”运行。

Module 和 Package:

在一个module下,可以声明多个包(package),一般命名规范如下:

    1.不要有中文

    2.不要以数字开头

    3.给包取名时一般都是公司域名倒着写,而且都是小写比如:尚硅谷网址是

       www.atguigu.com 那么我们的package包名应该写成:com.atguigu.子名字。

3. 快捷键的使用 

第1组:通用型

说明快捷键
复制代码-copyctrl + c
粘贴-pastectrl + v
剪切-cutctrl + x
撤销-undoctrl + z
反撤销-redoctrl + shift + z
保存-save allctrl + s
全选-select allctrl + a

第2组:提高编写速度(上)

说明快捷键
智能提示-editalt + enter
提示代码模板-insert live templatectrl+j
使用xx块环绕-surround with ...ctrl+alt+t
调出生成getter/setter/构造器等结构-generate ...alt+insert
自动生成返回值变量-introduce variable ...ctrl+alt+v
复制指定行的代码-duplicate line or selectionctrl+d
删除指定行的代码-delete linectrl+y
切换到下一行代码空位-start new lineshift + enter
切换到上一行代码空位-start new line before currentctrl +alt+ enter
向上移动代码-move statement upctrl+shift+↑
向下移动代码-move statement downctrl+shift+↓
向上移动一行-move line upalt+shift+↑
向下移动一行-move line downalt+shift+↓
方法的形参列表提醒-parameter infoctrl+p

第3组:提高编写速度(下)

说明快捷键
批量修改指定的变量名、方法名、类名等-renameshift+f6
抽取代码重构方法-extract method ...ctrl+alt+m
重写父类的方法-override methods ...ctrl+o
实现接口的方法-implements methods ...ctrl+i
选中的结构的大小写的切换-toggle casectrl+shift+u
批量导包-optimize importsctrl+alt+o

第4组:类结构、查找和查看源码

说明快捷键
如何查看源码-go to class...ctrl + 选中指定的结构 或 ctrl+n
显示当前类结构,支持搜索指定的方法、属性等-file structurectrl+f12
退回到前一个编辑的页面-backctrl+alt+←
进入到下一个编辑的页面-forwardctrl+alt+→
打开的类文件之间切换-select previous/next tabalt+←/→
光标选中指定的类,查看继承树结构-Type Hierarchyctrl+h
查看方法文档-quick documentationctrl+q
类的UML关系图-show uml popupctrl+alt+u
定位某行-go to line/columnctrl+g
回溯变量或方法的来源-go to implementation(s)ctrl+alt+b
折叠方法实现-collapse allctrl+shift+ -
展开方法实现-expand allctrl+shift+ +

第5组:查找、替换与关闭

说明快捷键
查找指定的结构ctlr+f
快速查找:选中的Word快速定位到下一个-find nextctrl+l
查找与替换-replacectrl+r
直接定位到当前行的首位-move caret to line starthome
直接定位到当前行的末位 -move caret to line endend
查询当前元素在当前文件中的引用,然后按 F3 可以选择ctrl+f7
全项目搜索文本-find in path ...ctrl+shift+f
关闭当前窗口-closectrl+f4

第6组:调整格式

说明快捷键
格式化代码-reformat codectrl+alt+l
使用单行注释-comment with line commentctrl + /
使用/取消多行注释-comment with block commentctrl + shift + /
选中数行,整体往后移动-tabtab
选中数行,整体往前移动-prev tabshift + tab

第7组:Debug快捷键

说明快捷键
单步调试(不进入函数内部)- step overF8
单步调试(进入函数内部)- step intoF7
强制单步调试(进入函数内部) - force step intoalt+shift+f7
选择要进入的函数 - smart step intoshift + F7
跳出函数 - step outshift + F8
运行到断点 - run to cursoralt + F9
继续执行,进入下一个断点或执行完程序 - resume programF9
停止 - stopCtrl+F2
查看断点 - view breakpointsCtrl+Shift+F8
关闭 - closeCtrl+F4

4. IDEA 常用插件

1. Translation:

注册翻译服务(有道智云、百度翻译开放平台、阿里云机器翻译)帐号,开通 翻译服务并获取其应用ID和密钥 绑定应用ID和密钥:偏好设置(设置) > 工具 > 翻译 > 常规 > 翻译引擎 > 配置…

使用:鼠标选中文本,点击右键即可自动翻译成多国语言。

注:请注意保管好你的应用密钥,防止其泄露。

2. Rainbow Brackets:

给括号添加彩虹色,使开发者通过颜色区分括号嵌套层级,便于阅读

3. CodeGlance Pro:

在编辑器右侧生成代码小地图,可以拖拽小地图光标快速定位代码,阅读行数 很多的代码文件时非常实用。

4. Key Promoter X:

快捷键提示插件。当你执行鼠标操作时,如果该操作可被快捷键代替,会给出 提示,帮助你自然形成使用快捷键的习惯,告别死记硬背。

5. Material Theme UI:  

对于很多人而言,写代码时略显枯燥的,如果能够安装自己喜欢的主题将为开 发工作带来些许乐趣。 IDEA 支持各种主题插件,其中最出名的当属 Material Theme UI。

四. Java基础语法 

1.Java 程序的入口

Java 程序的入口是main方法 
public static void main(String[] args){ }

2. 两种常见的输出语句:

换行输出语句:输出内容,完毕后进行换行,格式如下
System.out.println(输出内容)
直接输出语句:输出内容,完毕后不做任何处理,格式如下 
System.out.print(输出内容)
注意事项: 
换行输出语句,括号内可以什么都不写,只做换行处理 
直接输出语句,括号内什么都不写的话,编译报错

3. 源文件名与类名 

(1)源文件名是否必须与类名一致?public呢? 
     如果这个类不是public,那么源文件名可以和类名不一致。但是不便于代码维护。 
     如果这个类是public,那么要求源文件名必须与类名一致。否则编译报错。 
     我们建议大家,不管是否是public,都与源文件名保持一致,而且一个源文件尽量只写一个类,
     目的是为了好维护。 
(2)一个源文件中是否可以有多个类?public呢? 
     一个源文件中可以有多个类,编译后会生成多个.class字节码文件。 
     但是一个源文件只能有一个public的类。

4. 注释

1.单行注释:
//注释文字

2.多行注释:
/*  
注释文字1  
注释文字2 
注释文字3 
*/ 
注意:多行注释里面不允许有多行注释嵌套。

3.文档注释 (Java特有) :
/** 
@author  指定java程序的作者 
@version  指定源文件的版本 
*/  
文档注释内容可以被JDK提供的工具 javadoc 所解析,生成一套以网页文
件形式体现的该程序的说明文档。

5. 关键字

定义:被Java语言赋予了特殊含义,用做专门用途的字符串(或单词)例如: class、public 、 static 、 void 等,这些单词已经被Java定义好  了。

特点:全部关键字都是小写字母。

关键字比较多,不需要死记硬背,学到哪里记到哪里即可。

官方地址:https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html

说明: 1.  关键字一共50个,其中const和goto是保留字(reserved word)。

            2. true,false,null 不在其中,它们看起来像关键字,其实是字面量,表示特殊的布

                尔值和空值。

 6.  标识符

Java 中变量、方法、类等要素命名时使用的字符序列,称为标识符。

技巧:凡是自己可以起名字的地方都叫标识符。 

标识符的命名规则(必须遵守的硬性规定):

    > 由26个英文字母大小写,0-9 ,_或 $ 组成

    > 数字不可以开头。

    > 不可以使用关键字和保留字,但能包含关键字和保留字。

    > Java中严格区分大小写,长度无限制。

    > 标识符不能包含空格。

标识符的命名规范(建议遵守的软性要求,否则工作时容易被鄙视):

    > 包名:多单词组成时所有字母都小写:xxxyyyzzz。 例如:java.langcom.atguigu.bean

    > 类名、接口名:多单词组成时,所有单词的首字母大写:XxxYyyZzz 例如:

       HelloWorld,String,System 等

    > 变量名、方法名:多单词组成时,第一个单词首字母小写,第二个单词开始每个单词首

        字母大写:xxxYyyZzz 例如:age,name,bookName,main,binarySearch,getName

    > 常量名:所有字母都大写。多单词时每个单词用下划线连接:XXX_YYY_ZZZ 例如: 

       MAX_VALUE,PI,DEFAULT_CAPACITY

7.  变量

(1).变量的概念:

变量的概念: 

   –  内存中的一个存储区域,该区域的数据可以在同一类型范围内不断变化

   –  变量的构成包含三个要素:数据类型、变量名、存储的值

   –  Java 中变量声明的格式:数据类型 变量名 = 变量值

变量的作用:用于在内存中保存数据。

使用变量注意:

    – Java中每个变量必须先声明,后使用。

    – 使用变量名来访问这块区域的数据。

    – 变量的作用域:其定义所在的一对{ }内。

    – 变量只有在其作用域内才有效。出了作用域,变量不可以再被调用。

    – 同一个作用域内,不能定义重名的变量。

Java中变量的数据类型:

    基本数据类型:

         包括整数类型: byte \ short \ int \ long 、浮点数类型float \ double、字符类型char、布尔

         类型boolean

    引用数据类型:

         包括数组array、 类class、接口interface、枚举enum、注解annotation、记录record。

变量的声明:

格式:数据类型  变量名; 
//例如: 
//存储一个整数类型的年龄 
int age;  
//存储一个小数类型的体重 
double weight; 
//存储一个单字符类型的性别  
char gender; 
//存储一个布尔类型的婚姻状态 
boolean marry; 
//存储一个字符串类型的姓名 
String name; 
//声明多个同类型的变量 
int a,b,c; //表示a,b,c三个变量都是int类型。
注意:变量的数据类型可以是基本数据类型,也可以是引用数据类型。

变量的赋值:

给变量赋值,就是把“值”存到该变量代表的内存空间中。同时,给变量赋的值类型必须与变量声明的类型一致或兼容。

变量赋值的语法格式:

变量名 = 值; 
举例1:可以使用合适类型的常量值给已经声明的变量赋值 
age = 18; 
weight = 109; 
gender = '女'; 
举例2:可以使用其他变量或者表达式给变量赋值 
int m = 1; 
int n = m; 
int x = 1; 
int y = 2; 
int z = 2 * x + y; 
变量可以反复赋值 
//先声明,后初始化 
char gender; 
gender = '女'; 
//给变量重新赋值,修改gender变量的值 
gender = '男'; 
System.out.println("gender = " + gender);//gender = 男 
举例4:也可以将变量的声明和赋值一并执行 
boolean isBeauty = true; 
String name = "迪丽热巴"; 

(2). 基本数据类型

计算机存储单位

字节(Byte):是计算机用于计量存储容量的基本单位,一个字节等于8 bit。

位(bit):是数据存储的最小单位。二进制数系统中,每个0或1就是一个位,叫做bit (比

特),其中8 bit 就称为一个字节(Byte)。

转换关系: 8 bit = 1 Byte

                   1024 Byte = 1 KB

                   1024 KB = 1 MB

                   1024 MB = 1 GB

                   1024 GB = 1 TB

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

 Java 各整数类型有固定的表数范围和字段长度,不受具体操作系统的影响,以保证Java

 程序的可移植性。

 定义long类型的变量,赋值时需要以"l"或"L"作为后缀。 

 Java程序中变量通常声明为int型,除非不足以表示较大的数,才使用long。

 Java的整型常量默认为 int 型。

浮点类型:float、double 

与整数类型类似,Java 浮点类型也有固定的表数范围和字段长度,不受具体操作系统的

影响。

浮点型常量有两种表示形式:

      十进制数形式。如:5.12 512.0f .512 (必须有小数点)

      科学计数法形式。如:5.12e2 512E2 100E-2

float:单精度,尾数可以精确到7位有效数字。很多情况下,精度很难满足需求。

double:双精度,精度是float的两倍。通常采用此类型。

定义float 类型的变量,赋值时需要以"f"或"F"作为后缀。

Java 的浮点型常量默认为double型。

关于浮点型精度的说明:

    并不是所有的小数都能可以精确的用二进制浮点数表示。二进制浮点数不能精确的表示

    0.1、0.01、0.001 这样10 的负次幂。

    浮点类型float、double的数据不适合在不容许舍入误差的金融计算领域。如果需要精确数

    字计算或保留指定位数的精度,需要使用BigDecimal类。

字符类型:char 

char 型数据用来表示通常意义上“字符”(占2字节)

Java 中的所有字符都使用Unicode编码,故一个字符可以存储一个字母,一个汉字,或其

他书面语的一个字符。 

字符型变量的三种表现形式:

形式1:使用单引号(' ')括起来的单个字符。 
  例如:char c1 = 'a'; char c2 = '中'; char c3 = '9'; 
形式2:直接使用 Unicode值来表示字符型常量:‘\uXXXX’。其中,XXXX代表一个十六进制整数。 
  例如:\u0023 表示 '#'。
形式3:Java中还允许使用转义字符‘\’来将其后的字符转变为特殊字符型常量。 
  例如:char c3 = '\n'; // '\n'表示换行符 
形式4: 使用具体字符对于的数值(比如ASCII码)
  例如:char c=97; Stytem.out.println(c); 输出a

char类型是可以进行运算的。因为它都对应有Unicode码,可以看做是一个数值。

布尔类型:boolean

   boolean 类型用来判断逻辑条件,一般用于流程控制语句中

   boolean 类型数据只有两个值:true、false,无其它 

   不可以使用0或非 0 的整数替代false和true

   拓展:Java虚拟机中没有任何供boolean值专用的字节码指令,Java语言表达所操作的

              boolean值,在编译之后都使用java虚拟机中的int数据类 型来代替:true用1表示,

              false用0表示。——《java虚拟机规范 8版》

 (3)基本数据类型的运算规则

在Java 程序中,不同的基本数据类型(只有7种,不包含boolean类型)变量的值经常需要进行相互转换。

转换的方式有两种:自动类型提升和强制类型转换。

 自动类型提升

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

基本数据类型的转换规则如图所示:

当容量小的变量与容量大的变量做运算时,结果自动转换为容量大的数据类型。

特别的:byte、short类型的变量之间做运算,结果为int类型。

 强制类型转换

规则:将取值范围大(或容量大)的类型强制转换成取值范围小(或容量小) 的类型。

自动类型提升是Java自动执行的,而强制类型转换是自动类型提升的逆运算,需要我们自己手动执行。

转换格式: 数据类型 变量名 = (数据类型) 被强转数据值;

                   注意:()中的数据类型必须<=变量值的数据类型

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

int i = (int)3.14;//损失精度 
 
double d = 1.2; 
int num = (int)d;//损失精度 
 
int i = 200; 
byte b = (byte)i;//溢出

当某个值想要提升数据类型时,也可以使用强制类型转换。这种情况的强制类型转换是没有风险的,通常省略。

声明long类型变量时,可以出现省略后缀的情况。float则不同。

long l1 = 123L; 
long l2 = 123;如何理解呢? 此时可以看做是int类型的123自动类型提升为long 类型 
long l3 = 123123123123; 报错,因为123123123123 超出了int的范围。 
long l4 = 123123123123L; 
float f1 = 12.3; 报错,因为12.3看做是double,不能自动转换为float类型 
float f2 = 12.3F; 
float f3 = (float)12.3;

为什么标识符的声明规则里要求不能数字开头?

如果允许数字开头,则如下的声明编译就可以通过: 
int 123L = 12; 
进而,如下的声明中l的值到底是123?还是变量123L对应的取值12呢? 出现歧义了。 
long l = 123L; 

 (4)字符串类型

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

使用一对""来表示一个字符串,内部可以包含0个、1个或多个字符。

声明方式与基本数据类型类似。例如:String str = “尚硅谷”;  

运算规则:

任意八种基本数据类型的数据与String类型只能进行连接“+”运算,且结果  一定也是String类型

System.out.println("" + 1 + 2);//12 
int num = 10; 
boolean b1 = true; 
String s1 = "abc"; 
String s2 = s1 + num + b1; 
System.out.println(s2);//abc10true 
String s3 = num + b1 + s1;//编译不通过,因为int类型不能与boolean运算 
String s4 = num + (b1 + s1);//编译通过

String 类型不能通过强制类型()转换,转为其他的类型

String str = "123"; 
int num = (int)str;//错误的 
int num = Integer.parseInt(str);//正确的,后面才能讲到,借助包装类的方法才能转

8. 运算符

运算符的分类:算术运算符、赋值运算符、比较(或关系)运算符、逻辑运算符、位运算符、条件运算符、Lambda运算符

(1)算数运算符 

“+”号的两种用法:

      第一种:对于+两边都是数值的话,+就是加法的意思

      第二种:对于+两边至少有一边是字符串的话,+就是拼接的意思  

(2)赋值运算符

符号:=

    当“=”两侧数据类型不一致时,可以使用自动类型转换或使用强制类型转换原则进行处理。

    支持连续赋值。 扩展赋值运算符: +=、 -=、*=、 /=、%=

扩展赋值运算符: +=、 -=、*=、 /=、%=

(3)比较(关系)运算符

比较运算符的结果都是boolean型,也就是要么是true,要么是false。

> < >= <= :只适用于基本数据类型(除boolean类型之外)

==   != :适用于基本数据类型和引用数据类型 

比较运算符“==”不能误写成“=”

(4)逻辑运算符

逻辑运算符,操作的都是boolean类型的变量或常量,而且运算得结果也是boolean类型的值。

运算符说明:

      & 和 &&:表示"且"关系,当符号左右两边布尔值都是true时,结果才能  为true。否则,

      为false。

      | 和 || :表示"或"关系,当符号两边布尔值有一边为true时,结果为 true。当两边都为

      false时,结果为false

      ! :表示"非"关系,当变量布尔值为true时,结果为false。当变量布尔值 为false 时,结果

      为true。

      ^ :当符号左右两边布尔值不同时,结果为true。当两边布尔值相同时, 结果为false。

区分“&”和“&&”: 

      相同点:如果符号左边是true,则二者都执行符号右边的操作

      不同点:& : 如果符号左边是false,则继续执行符号右边的操作

                    && :如果符号左边是false,则不再继续执行符号右边的操作  

区分“|”和“||”:

      相同点:如果符号左边是false,则二者都执行符号右边的操作

      不同点:| : 如果符号左边是true,则继续执行符号右边的操作

                    ||:如果符号左边是true,则不再继续执行符号右边的操作 

(5)位运算符(难点、非重点) 

位运算符的运算过程都是基于二进制的补码运算

左移:<< 运算规则:在一定范围内,数据每向左移动一位,相当于原数据*2。(正数、 负

           数都适用)  

         【注意】当左移的位数n超过该数据类型的总位数时,相当于左移(n-总位 数)位

右移:>> 运算规则:在一定范围内,数据每向右移动一位,相当于原数据/2。(正数、 负数

          都适用)  

         【注意】如果不能整除,向下取整。

无符号右移:>>> 运算规则:往右移动后,左边空出来的位直接补0。(正数、负数都适用)

按位与:&  运算规则:对应位都是1才为1,否则为0。

按位或:| 运算规则:对应位只要有1即为1,否则为0。

按位异或:^ 运算规则:对应位一个为1一个为0,才为1,否则为0。

按位取反:~ 运算规则:对应位为1,则结果为0;对应位为0,则结果为1。

(6)条件运算符

条件运算符格式: (条件表达式)? 表达式1:表达式2

说明:条件表达式是boolean类型的结果,根据boolean的值选择表达式1或表达式2

与 if-else 的转换关系:

    凡是可以使用条件运算符的地方,都可以改写为if-else结构。反之,不成立。

(7)运算符优先级

运算符有不同的优先级,所谓优先级就是在表达式运算中的运算符顺序。

如果想使优先级比较高,使用()

 9. 流程控制语句

流程控制语句是用来控制程序中各语句执行顺序的语句,可以把语句组合成能完成一 定功能的小逻辑模块。

 (1)if-else 条件判断结构

if(条件表达式){ 
   语句块; 
} 

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

if (条件表达式1) { 
   语句块1; 
} else if (条件表达式2) { 
   语句块2; 
} 
... 
}else if (条件表达式n) { 
  语句块n; 
} else { 
   语句块n+1; 
} 
说明:条件表达式必须是布尔表达式(关系表达式或逻辑表达式)或 布尔变量。
      语句块只有一条执行语句时,一对{}可以省略,但建议保留
使用Scanner类从键盘获取数据
1. 导包
   import java.util.Scanner;
2. 创建一个Scanner类的实例
   Scanner scanner=new Scanner(System.in);
3. 调用Scanner类中的方法,获取指定类型的变量
   int a=scanner.nextInt(); 阻塞式方法
4. 输出
   System.out.println(a);
5. scanner.close()  为了防止内存泄漏释放资源

Scanner类中提供了获取byte\short\int\long\float\double\boolean\String类型变量的方法。
没有提供获取char类型变量的方法。需要使思next().charAt(0)

注意:需要根据相应的方法,来输入指定类型的值。如果输入的数据类型与要求的类型不匹配时,
会报异常导致程序终止。 
   获取随机数
Math.random()会返回一个[0.0 , 1.0)范围的double型的随机数

获取一个[a,b]范围的随机整数:
(int)(Math.random())*(b-a+1)+a)

 (2)switch-case 选择结构

语法格式: 
  switch(表达式){ 
      case 常量值1: 语句块1; break; 
      case 常量值2: 语句块2; break;  
      ... 
      default: 语句块n+1; break; 
  } 

执行过程: 
  第1步:根据switch中表达式的值,依次匹配各个case。如果表达式的值等于某个case中的常量值,
         则执行对应case中的执行语句。 
  第2步:执行完此case的执行语句以后
         情况1:如果遇到break,则执行break 并跳出当前的switch-case结构   
         情况2:如果没有遇到break,则会继续执行当前case之后的其它case中的执行语句。
                --->case穿透...直到遇到break关键字或执行完所有的case及default的执行语句,
                跳出当前的switch-case 结构

使用注意点: 
   switch(表达式)中表达式的值必须是下述几种类型之一:byte,short,char,int,枚举 
   (jdk 5.0),String (jdk 7.0); 
   case子句中的值必须是常量,不能是变量名或不确定的表达式值或范围; 
   同一个switch语句,所有case子句中的常量值互不相同; 
   break语句用来在执行完一个case分支后使程序跳出switch语句块; 
   如果没有break,程序会顺序执行到switch结尾; 
   default子句是可选的。同时,位置也是灵活的。当没有匹配的case时,执行default语句。 

(3)for 循环

语法格式: 
for (①初始化部分; ②循环条件部分; ④迭代部分){ 
     ③循环体部分; 
} 
执行过程:①-②-③-④-②-③-④-②-③-④-.....-②

说明: 
  for(;;)中的两个;不能多也不能少 
  初始化部分可以声明多个变量,但必须是同一个类型,用逗号分隔 
  循环条件部分为boolean类型表达式,当值为false时,退出循环 
  可以有多个变量更新,用逗号分隔 
  我们可以在循环结构中使用break。一旦执行break,就跳出(或结束)当前循环结构。
  continue 一旦执行,就结束(或跳出)当前循环结构      

带标签的使用:
  break 语句出现在多层嵌套的语句块中时,可以通过标签指明要终止的是哪一层语句块  
  continue语句出现在多层嵌套的循环语句体中时,也可以通过标签指明要跳过的是哪一层循环。 
  label1: {   ……         
  label2:      {   ……  
  label3:         {   ……  
                    break label2; 
                    …… 
                  }
               }
          }

(4)while循环和do-while循环

while循环
语法格式: 
①初始化部分 
while(②循环条件部分){ 
   ③循环体部分; 
   ④迭代部分; 
} 
执行过程:①-②-③-④-②-③-④-②-③-④-...-②
while(循环条件)中循环条件必须是boolean类型。 
注意不要忘记声明④迭代部分。否则,循环将不能结束,变成死循环。 

do-while循环
语法格式: 
①初始化部分; 
do{ 
   ③循环体部分 
   ④迭代部分 
}while(②循环条件部分);  
执行过程:①-③-④-②-③-④-②-③-④-...-② 
结尾while(循环条件)中循环条件必须是boolean类型 
do{}while();最后有一个分号 
do-while结构的循环体语句是至少会执行一次,这个和for和while是不一样的

long start = System.currentTimeMillis(); 记录当前时间距离1970-1-1 00:00:00 的毫秒数 

10. 数组 

(1)数组概念:

数组(Array):是多个相同类型数据按一定顺序排列的集合,并使用一个名字命名,并通过编号的方式对这些数据进行统一管理。  

数组中的概念:  数组名 

                           下标(或索引)

                           元素

                           数组的长度

数组的特点:

       • 数组本身是引用数据类型,而数组中的元素可以是任何数据类型,包括基本数据类型和

         引用数据类型。

       • 创建数组对象会在内存中开辟一整块连续的空间。占据的空间的大小,取决于数组的

         长度和数组中元素的类型。

       • 数组中的元素在内存中是依次紧密排列的,有序的。 

       • 数组,一旦初始化完成,其长度就是确定的。数组的长度一旦确定,就不能修改。

       • 我们可以直接通过下标(或索引)的方式调用指定位置的元素,速度很快。

       • 数组名中引用的是这块连续空间的首地址。

数组的分类:

      按照元素类型分: • 基本数据类型元素的数组

                                    • 引用数据类型元素的数组:每个元素位置存储对象

      按照维度分:一维数组和二维数组

 (2)一维数组:

 一维数组的使用:

一维数组的声明 
格式: 元素的数据类型[] 一维数组的名称; 
      元素的数据类型  一维数组名[];
举例: 
    int[] arr; 
    int arr1[]; 
    double[] arr2; 
    String[] arr3;  引用类型变量数组
数组的元素类型:即创建的数组容器可以存储什么数据类型的数据,
               元素的类型可以是任意的Java的数据类型。
               例如:int、String、Student等。

静态初始化: 
  如果数组变量的初始化和数组元素的赋值操作同时进行,那就称为静态初始化。 
  静态初始化,本质是用静态数据(编译时已知)为数组初始化。此时数组的长度由静
  态数据的个数决定。 
  一维数组声明和静态初始化格式1: 
     数据类型[] 数组名 = new 数据类型[]{元素1,元素2,元素3,...}; 
     或   
     数据类型[] 数组名;  
     数组名 = new 数据类型[]{元素1,元素2,元素3,...}; 
     new:关键字,创建数组使用的关键字。因为数组本身是引用数据类型,
          所以要用new创建数组实体。
  一维数组声明和静态初始化格式2: 
     数据类型[] 数组名 = {元素1,元素2,元素3...};
     注意:必须在一个语句中完成,不能分成两个语句写 
     int[] arr = {1,2,3,4,5};正确 
     int[] arr; arr = {1,2,3,4,5};错误

动态初始化:
   数组变量的初始化和数组元素的赋值操作分开进行,即为动态初始化。 
   动态初始化中,只确定了元素的个数(即数组的长度),而元素值此时只是默认值,
   还并未真正赋自己期望的值。真正期望的数据需要后续单独一个一个赋值
   格式:
     数组存储的元素的数据类型[] 数组名字 = new 数组存储的元素的数据类型[长度]; 
     或 
     数组存储的数据类型[] 数组名字;  数组名字 = new 数组存储的数据类型[长度]; 
     [长度]:数组的长度,表示数组容器中可以最多存储多少个元素。
     注意:数组有定长特性,长度一旦指定,不可更改。
举例1:正确写法 
      int[] arr = new int[5]; 
      int[] arr;  arr = new int[5]; 
举例2:错误写法 
      int[] arr = new int[5]{1,2,3,4,5};
      错误的,后面有{}指定元素列表,就不需要在[]中指定元素个数了。 

数组的长度:
   数组的元素总个数,即数组的长度 
   每个数组都有一个属性length指明它的长度
   每个数组都具有长度,而且一旦初始化,其长度就是确定,且是不可变的。

如何表示数组中的一个元素? 
   每一个存储到数组的元素,都会自动的拥有一个编号,从0开始,这个自动编号称为
   数组索引(index)或下标,可以通过数组的索引/下标访问到数组中的元素。 
   数组名[索引/下标] 

数组的下标范围? 
   Java 中数组的下标从[0]开始,下标范围是[0, 数组的长度-1],即[0, 数组名.length-1] 
   数组元素下标可以是整型常量或整型表达式。如a[3] , b[i] , c[6*i]; 

一维数组元素的默认值:

数组是引用类型,当我们使用动态初始化方式创建数组时,元素值只是默认值。
对于基本数据类型而言,默认初始化值各有不同。 
对于引用数据类型而言,默认初始化值为null(注意与0不同!) 

一维数组内存分析:

     Java虚拟机的内存划分:为了提高运算效率,就对空间进行了不同区域的划分,因为每一

    片区域都有特 定的处理数据方式和内存管理方式。

   与目前数组相关的内存结构:

        比如:int[] arr = new int[ ]{1,2,3};
                  虚拟机栈:用于存放方法中声明的变量。比如:arr

                  堆:用于存放数组的实体(即数组中的所有元素)。比如:1,2,3

    一个一维数组内存图:

public static void main(String[] args) { 
   int[] arr = new int[3]; 
   System.out.println(arr);//[I@5f150435 
} 

   两个一维数组内存图:

  两个变量指向一个一维数组:

(3)二维数组:

对于二维数组的理解,可以看成是一维数组array1又作为另一个一维数组array2的元素而存在。

其实,从数组底层的运行机制来看,其实没有多维数组。

声明:
   元素的数据类型[][] 二维数组的名称;   推荐 
   元素的数据类型  二维数组名[][];   不推荐 
   元素的数据类型[]  二维数组名[];   不推荐 

面试: 
   int[] x, y[];   x 是一维数组,y是二维数组

静态初始化:
   int[][] arr = new int[][]{{3,8,2},{2,7},{9,0,1,6}}; 
   定义一个名称为arr的二维数组,二维数组中有三个一维数组 
   每一个一维数组中具体元素也都已初始化 
   第一个一维数组 arr[0] = {3,8,2}; 
   第二个一维数组 arr[1] = {2,7}; 
   第三个一维数组 arr[2] = {9,0,1,6}; 
   第三个一维数组的长度表示方式:arr[2].length; 
   int[][] arr = new int[][]{{1,2,3},{4,5,6},{7,8,9,10}}; 
   int[][] arr;   arr = new int[][]{{1,2,3},{4,5,6},{7,8,9,10}}; 
   int[][] arr = {{1,2,3},{4,5,6},{7,8,9,10}}; 

动态初始化:
   如果二维数组的每一个数据,甚至是每一行的列数,需要后期单独确定,那么
   就只能使用动态初始化方式了。
   动态初始化方式分为两种格式:
   格式1:每一行的列数是相同的
          元素的数据类型[][] 二维数组名 = new 元素的数据类型[m][n]; 
          二维数组名[行下标][列下标] = 值; 
          此时外层元素初始值是地址值,内层元素初始值和一维数组默认值规定一样
   格式2:每一行的列数不一样 
          元素的数据类型[][] 二维数组名 = new 元素的数据类型[总行数][];
          二维数组名[行下标] = new 元素的数据类型[该行的总列数]; 
          二维数组名[行下标][列下标] = 值; 
          外层元素,默认存储null
          内层元素,不存在的。如果调用会报错(NullPointerException)|

二维数组的长度和角标:
   二维数组的长度/行数:二维数组名.length
   二维数组的某一行:二维数组名[行下标],此时相当于获取其中一组数据。它本质上是一
   个一维数组。行下标的范围:[0, 二维数组名.length-1]。此时把二维数组看成一维数
   组的话,元素是行对象。 
   某一行的列数:二维数组名[行下标].length,因为二维数组的每一行是一个一维数组。 
   某一个元素:二维数组名[行下标][列下标],即先确定行/组,再确定列。 

两个二维数组的维数不同或类型不同不能直接赋值

二维数组的内存解析:

(4)Arrays 工具类的使用:

java.util.Arrays 类即为操作数组的工具类,包含了用来操作数组(比如排序和搜索)的各种方法。 比如:

int[] a=new int[]{1,2,3};
int[] b=new int[]{1,2,3};
System.out.println(Arrays.equals(a,b)); 比较两个数组的元素是否依次相等,返回布尔类型值
System.out.println(Arrays.toString(a)); 输出数组元素信息。
Arrays.fill(a,1); 将指定值替换掉数组所有元素
Arrays.sort(a);将数组进行升序排序
Arrays.binarySearch(a,key);在数组中查找 key 是否存在,如果存在返回第一次找到的下标,不存
                           在返回负数。

(5)数组中的常见异常:

数组角标越界的异常:ArrayIndex0ut0fBoundsException
空指针的异常:NullPointerException

11. 面向对象

(1)面向对象编程概述

学习面向对象内容的三条主线

    • Java类及类的成员:(重点)属性、方法、构造器;(熟悉)代码块、内部类

    • 面向对象的特征:封装、继承、多态、(抽象)

    • 其他关键字的使用:this、super、package、import、static、final、interface、 abstract

      等

面向对象,是软件开发中的一类编程风格、开发范式。除了面向对象,还有面向过程、指令式编程和函数式编程。在所有的编程范式中,我们接触最多的还是面向过程和面向对象两种。 

面向过程的程序设计思想(Process-Oriented Programming),简称POP关注的焦点是过程:过程就是操作数据的步骤。如果某个过程的实现代码重复出现,那么就可以把这个过程抽取为一个函数。这样就可以大大简化冗余代码,便于维护。 典型的语言:C语言代码结构:以函数为组织单位。 是一种“执行者思维”,适合解决简单问题。扩展能力差、后期维护难度较大。

面向对象的程序设计思想( Object Oriented Programming),简称OOP 关注的焦点是类:在计算机程序设计过程中,参照现实中事物,将事物的属性特征、行为特征抽象出来,用类来表示。 典型的语言:Java、C#、C++、Python、Ruby和PHP等 代码结构:以类为组织单位。每种事物都具备自己的属性和行为/功能。 是一种“设计者思维”,适合解决复杂问题。代码扩展性强、可维护性高。

面向对象可以帮助我们从宏观上把握、从整体上分析整个系统。 但是, 具体到实现部分的微观操作(就是一个个方法),仍然需要面向过程的思路去处理。

注意: 我们千万不要把面向过程和面向对象对立起来。他们是相辅相成的。 面向对象离不开面向过程!

类比举例:人把大象装进冰箱
面向过程: 
   1.打开冰箱 
   2.把大象装进冰箱 
   3.把冰箱门关住 
面向对象:
  人{ 
    打开(冰箱){ 
      冰箱.开门();  
    } 
    操作(大象){ 
      大象.进入(冰箱); 
    } 
    关闭(冰箱){    
      冰箱.关门();      
    }
  } 
  冰箱{ 
    开门(){ }   
    关门(){ } 
  }
  大象{ 
    进入(冰箱){  } 
  } 

Java 语言的基本元素:类和对象

类(Class)和对象(Object)是面向对象的核心概念。

什么是类:具有相同特征的事物的抽象描述,是抽象的、概念上的定义。

什么是对象:实际存在的该类事物的每个个体,是具体的,因而也称为实例

(2)类的实例化与对象的内存解析

类的成员概述:

     面向对象程序设计的重点是类的设计 

     类的设计,其实就是类的成员的设计

Java中用类class来描述事物也是如此。类,是一组相关属性和行为的集合,这也是类最基本的两个成员。

     属性:该类事物的状态信息。对应类中的成员变量              成员变量<—>属性<—> Field

     行为:该类事物要做什么操作,或者基于事物的状态能做什么。对应类中的成员方法

                (成员)方法<—>函数<—>Method

类的定义使用关键字:class。
格式如下: 
[修饰符] class 类名{ 
    属性声明; 
    方法声明; 
} 
例如:
public class Person {
    int age;  声明属性
    public void eat(){  声明方法
        System.out.println("吃饭");
    }
}

对象的创建:
类名 对象名 = new 类名();
class PersonTest{ 
    public static void main(String[] args){ 
        Person per = new Person(); 创建Person类的对象 
        Dog dog = new Dog();  创建Dog类的对象 
    } 
} 

对象调用属性或方法:
对象是类的一个实例,必然具备该类事物的属性和行为(即方法)。 
使用"对象名.属性" 或 "对象名.方法"的方式访问对象成员(包括属性和方法)
例如: 
public class Animal { 声明Animal类 
    public int legs; 
    public void eat() { 
        System.out.println("Eating."); 
    } 
    public void move() { 
        System.out.println("Move."); 
    } 
} 
public class AnimalTest {  声明测试类 
    public static void main(String args[]) { 
        Animal xb = new Animal();  创建对象 
        xb.legs = 4;  访问属性 
        System.out.println(xb.legs); 
        xb.eat();  访问方法 
        xb.move();  访问方法 
    } 
} 

匿名对象:
我们也可以不定义对象的句柄,而直接调用这个对象的方法。这样的对象叫做匿名对象。
new Person().shout(); 
如果一个对象只需要进行一次方法调用,那么就可以使用匿名对象。 
我们经常将匿名对象作为实参传递给一个方法调用。 

对象的内存解析:

JVM 内存结构划分,HotSpot Java 虚拟机的架构图如下。其中我们主要关心的是运行时数据区部分 (Runtime Data Area)。

其中:

    堆(Heap):new出来的结构(比如:数组实体,对象实体)。包括对象属性

    栈(Stack):方法内定义的变量,存储在栈中。

    方法区(Method Area):用于存储已被虚拟机加载的类信息、常量、静态变 量、即时编

    译器编译后的代码等数据。存放类的模板。比如:Person类的模板

对象名中存储的是什么?

直接打印对象名和数组名都是显示“类型@对象的hashCode值",所以说类、数组都是引用数据类型,引用数据类型的变量中存储的是对象的地址,或者说指向堆中对象的首地址。

(3)成员变量与局部变量的对比

如何声明成员变量:

语法格式: 
[修饰符1] class 类名{  
    [修饰符2] 数据类型 成员变量名 [= 初始化值];  
} 

说明:
  位置要求:必须在类中,方法外
  修饰符2(暂不考虑)常用的权限修饰符有:private、缺省、protected、public 
                   其他修饰符:static、final  
  数据类型:任何基本数据类型(如int、Boolean) 或 任何引用数据类型。 
  成员变量名:属于标识符,符合命名规则和规范即可。 
  初始化值:根据情况,可以显式赋值;也可以不赋值,使用默认值

例如:
public class Person{ 
   private int age;  声明private 变量 age   
   public String name = “Lila”;  声明public变量 name  
} 

成员变量和局部变量:

   在方法体外,类体内声明的变量称为成员变量。 
   在方法体内部等位置声明的变量称为局部变量。

  其中,static 可以将成员变量分为两大类,静态变量和非静态变量。其中静态变量又称为类

  变量,非静态变量又称为实例变量或者属性。

成员变量与局部变量的对比:

   相同点:  变量声明的格式相同: 数据类型 变量名 = 初始化值

                   变量必须先声明、后初始化、再使用。

                   变量都有其对应的作用域。只在其作用域内是有效的

   不同点:

                1、声明位置和方式

                        实例变量:在类中方法外

                        局部变量:在方法体 {}中或方法的形参列表、代码块中

                2、在内存中存储的位置不同

                        实例变量:堆

                        局部变量:栈

                3、生命周期

                        实例变量:和对象的生命周期一样,随着对象的创建而存在,随着对象被GC

                                          回收而消亡, 而且每一个对象的实例变量是独立的。

                        局部变量:和方法调用的生命周期一样,每一次方法被调用而在存在,随着

                                          方法执行的结束而消亡, 而且每一次方法调用都是独立。

                4、作用域

                       实例变量:通过对象就可以使用,本类中直接调用,其他类中 “对象.实例变量”

                       局部变量:仅限于声明此局部变量所在的方法(或构造器、代码块)中

               5、修饰符(后面来讲)

                      实例变量: public,protected,private,final,volatile,transient 等

                      局部变量:final

               6、默认值

                      实例变量:有默认值

                      局部变量:没有默认初始化值,在使用局部变量之前,必须要显式的赋值,否则

                                     报错。对于方法的形参而言,在调用方法时,给此形参赋值即可。

 (4)方法的作用与方法的声明:

方法是类或对象行为特征的抽象,用来完成某个功能操作。在某些语言中也称为函数或过程。 将功能封装为方法的目的是,可以实现代码重用,减少冗余,简化代码

Java 里的方法不能独立存在,所有的方法必须定义在类里。 

如何声明方法:

声明方法的语法格式: 
   [修饰符] 返回值类型 方法名([形参列表])[throws 异常列表]{ 
               方法体的功能代码 
   } 
   一个完整的方法 = 方法头 + 方法体。 
   方法头就是[修饰符] 返回值类型 方法名([形参列表])[throws 异常列表],也称为
   方法签名。通常调用方法时只需要关注方法头就可以,从方法头可以看出这个方法的
   功能和调用格式。 
   方法体就是方法被调用后要执行的代码。对于调用者来说,不了解方法体如何实现的,
   并不影响方法的使用。

方法头可能包含5个部分:
   修饰符:可选的。方法的修饰符也有很多,例如:public、protected、private、static、
           abstract、native、final、synchronized 等,后面会一一学习。 其中,权限
           修饰符有public、protected、private。在讲封装性之前,我们先默认使用pulbic
           修饰方法。 
           其中,根据是否有static,可以将方法分为静态方法和非静态方法。其中静
           态方法又称为类方法,非静态方法又称为实例方法。咱们在讲static前先
           学习实例方法。 
   返回值类型: 表示方法运行的结果的数据类型,方法执行后将结果返回到调用者。 
               无返回值,则声明:void。有返回值,则声明出返回值类型(可以是任意类型)。
               与方法体中“return 返回值”搭配使用 
   方法名:属于标识符,命名时遵循标识符命名规则和规范,“见名知意” 
   形参列表:表示完成方法体功能时需要外部提供的数据列表。可以包含零个,一个或多个
            参数。 
            无论是否有参数,()不能省略 
            如果有参数,每一个参数都要指定数据类型和参数名,多个参数之间使用
            逗号分隔,例如: 
                         一个参数: (数据类型 参数名) 
                         二个参数: (数据类型1 参数1, 数据类型2 参数2)  
            参数的类型可以是基本数据类型、引用数据类型 
   throws 异常列表:可选,在以后讲

方法体:方法体必须有{}括起来,在{}中编写完成方法功能的代码

关于方法体中return语句的说明:
  return 语句的作用是结束方法的执行,并将方法的结果返回去
  如果返回值类型不是void,方法体中必须保证一定有 return 返回值; 语句,并且要求该返
  回值结果的类型与声明的返回值类型一致或兼容。 
  如果返回值类型为void时,方法体中可以没有return语句,如果要用return语句提前结
  束方法的执行,那么return后面不能跟返回值,直接写return ; 就可以。
  return 语句后面就不能再写其他代码了,否则会报错:Unreachable code

方法的分类:按照是否有形参及返回值:

如何调用实例方法:

  方法通过方法名被调用,且只有被调用才会执行。

  方法调用语法格式: 对象.方法名([实参列表])

调用方法的注意点: 

    必须先声明后使用,且方法必须定义在类的内部

    调用一次就执行一次,不调用不执行。

    方法中可以调用类中的方法或属性,不可以在方法内部定义方   法。

方法调用内存分析:

    方法没有被调用的时候,都在方法区中的字节码文件(.class)中存储。

    方法被调用的时候,需要进入到栈内存中运行。方法每调用一次就会在栈中有一个入栈

    动作,即给当前方法开辟一块独立的内存区域,用于存储当前方法的局部变量的值。

    当方法执行结束后,会释放该内存,称为出栈,如果方法有返回值,就会把结果返回调用

    处,如果没有返回值,就直接结束,回到调用处继续执行下一条指令。

    栈结构:先进后出,后进先出。

  (5)对象数组及内存解析

数组的元素可以是基本数据类型,也可以是引用数据类型。当元素是引用类型中的类时,我们称为对象数组。

对象数组,首先要创建数组对象本身,即确定  数组的长度,然后再创建每一个元素对象,如果不创建,数组的元素的默认值就是null,所以很容易出现空指针异常NullPointerException。

(6)方法的重载

方法重载:在同一个类中,允许存在一个以上的同名方法,只要它们的参数列表不同即可。

                  参数列表不同,意味着参数个数或参数类型的不同  

                  满足这样特征的多个方法,彼此之间构成方法的重载。

总结为"两同一不同":

            两同:同一个类、相同的方法名
            一不同:参数列表不同。①参数个数不同  ②参数类型不同

            注意:方法的重载与形参的名、权限修饰符、返回值类型都没有关系。

如何判断两个方法是相同的呢?  
     方法名相同,且形参列表相同。(形参列表相同指的是参数个数和类型都相同,与形参

     名没关系)

     在一个类中,允许存在多个相同名字的方法,只要他们的形参列表不同即可。

编译器是如何确定调用的某个具体的方法呢?

     先通过方法名确定了一波重载的方法,进而通过不同的形参列表,确定具体的某一个方法

举例:

System.out.println()方法就是典型的重载方法,其内部的声明形式如下:
System.out.println(3); 
System.out.println(1.2f); 
System.out.println("hello!");

返回两个整数的和 
public int add(int x,int y){ 
return x+y; 
} 
返回三个整数的和 
public int add(int x,int y,int z){ 
return x+y+z; 
} 
返回两个小数的和 
public double add(double x,double y){ 
return x+y; 
}

面试题:

int[] arr=new int[]{1,2,3};
System.out.println(arr);  打印地址
char[] arr1=new  char[]{'1','2','3'};
System.out.println(arr1); 打印所有字符
boolean[] arr2=new  boolean[]{true,false,true};
System.out.println(arr2); 打印地址

(7)可变个数的形参 

在JDK 5.0 中提供了Varargs(variable number of arguments)机制。即当定义一个方法时,形参的类型可以确定,但是形参的个数不确定,那么可以考虑使用可变个数的形参。

格式: 方法名(参数的类型名 ...参数名)

说明:   
    ①可变个数形参的方法在调用时,针对于可变的形参赋的实参的个数可以为:0个、1个或

       多个

    ②可变个数形参的方法与同一个类中,同名的多个方法之间可以构成重载
    ③特例:可变个数形参的方法与同一个类中方法名相同,且与可变个数形参的类型相同的

        数组参数不构成重载。

    ④可变个数的形参必须声明在形参列表的最后
    ⑤可变个数的形参最多在一个方法的形参列表中出现一次

JDK 5.0 以前:采用数组形参来定义方法,传入多个同一类型变量 
public static void test(int a ,String[] books); 
JDK5.0:采用可变个数形参来定义方法,传入多个同一类型变量 
public static void test(int a ,String...books); 

遍历所有参数都是:
for(int i=0;i<books.length;i++){
     System.out.println(books[i]);
}

(8)方法的参数传递机制 

参数传递机制:值传递

Java 里方法的参数传递方式只有一种:值传递。 即将实际参数值的副本(复制品)传入方法内,而参数本身不受影响。

形参是基本数据类型:将实参基本数据类型变量的“数据值”传递给形参

形参是引用数据类型:将实参引用数据类型变量的“地址值”传递给形参 

(9)递归(recursion)方法

递归方法调用:方法自己调用自己的现象就称为递归。

递归的分类: 直接递归、间接递归。 

直接递归:方法自身调用自己。 
public void methodA(){ 
   methodA(); 
} 
间接递归:可以理解为A()方法调用B()方法,B()方法调用C()方法,C()方法调用A()方法。 
public static void A(){ 
   B(); 
} 
public static void B(){ 
   C(); 
} 
public static void C(){ 
   A(); 
}

递归方法包含了一种隐式的循环。

递归方法会重复执行某段代码,但这种重复执行无须循环控制。

递归一定要向已知方向递归,否则这种递归就变成了无穷递归,停不下来,类似于死循环。最终发生栈内存溢出。

递归调用会占用大量的系统堆栈,内存耗用多,在递归调用层次多时速度要比循环慢的多,所以在使用递归时要慎重。

在要求高性能的情况下尽量避免使用递归,递归调用既花时间又耗内存。考虑使用循环迭代

 (10)关键字:package、import的使用

package(包)

package,称为包,用于指明该文件中定义的类、接口等结构所在的包。

语法格式: package 顶层包名.子包名 ;

说明:

       一个源文件只能有一个声明包的package语句

       package语句作为Java源文件的第一条语句出现。若缺省该语句,则指定为无名包。

       包名,属于标识符,满足标识符命名的规则和规范(全部小写)、见名知意

              – – 包通常使用所在公司域名的倒置:com.atguigu.xxx。

              取包名时不要使用"java.xx"包

       包对应于文件系统的目录,package语句中用 “.” 来指明包(目录)的层次,每.一次就表示

       一层文件目录。

       同一个包下可以声明多个结构(类、接口),但是不能定义同名的结构(类、接口)

       不同的包下可以定义同名的结构(类、接口)

包的作用

       包可以包含类和子包,划分项目层次,便于管理

       帮助管理大型软件系统:将功能相近的类划分到同一个包中。比如:MVC的设计模式

       解决类命名冲突的问题

       控制访问权限

JDK 中主要的包介绍:

java.lang包含一些Java语言的核心类,如String、Math、Integer、System和Thread提供常用功能 
java.net包含执行与网络相关的操作的类和接口。 
java.io包含能提供多种输入/输出功能的类。 
java.util包含一些实用工具类,如定义系统特性、接口的集合框架类、使用与日期日历相关的函数。 
java.text包含了一些java格式化相关的类 
java.sql包含了java进行JDBC数据库编程的相关类/接口 
java.awt包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构
建和管理应用程序的图形用户界面(GUI)。 

import(导入):

为了使用定义在其它包中的Java类,需用import语句来显式引入指定包下所需要的类。相当于import语句告诉编译器到哪里去寻找这个类。

语法格式: import 包名.类名;  

注意事项:

     import语句,声明在包的声明和类的声明之间。

     如果需要导入多个类或接口,那么就并列显式多个import语句即可

     如果使用a.*导入结构,表示可以导入a包下的所有的结构。

          举例:可以使用 java.util.*的方式,一次性导入util 包下所有的类或接口。

     如果导入的类或接口是java.lang包下的,或者是当前包下的,则可以省略此import语句。       如果已经导入java.a包下的类,那么如果需要使用a包的子包下的类的话,仍然需要导入。

     如果在代码中使用不同包下的同名的类,那么就需要使用类的全类名的方式指明调用的是

     哪个类。

   (了解)import static组合的使用:调用指定类或接口下的静态的属性或方法

(11) 面向对象特征一:封装性(encapsulation)

随着我们系统越来越复杂,类会越来越多,那么类之间的访问边界必须把握好,面向对象的开发原则要遵循“高内聚、低耦合”。

为什么需要封装性? 

    高内聚:类的内部数据操作细节自己完成,不允许外部干涉;

    低耦合:仅暴露少量的方法给外部使用,尽量方便外部调用。

    所谓封装,就是把客观事物封装成抽象概念的类,并且类可以把自己的数据和方法只向可

    信的类或者对象开放,向没必要开放的类或者对象隐藏信息。 

    通俗的讲,把该隐藏的隐藏起来,该暴露的暴露出来。这就是封装性的设计思想。

Java 如何实现数据封装?

   实现封装就是控制类或成员的可见性范围。这就需要依赖访问控制修饰符,也称为权限修

   饰符来控制。

   权限修饰符:public、protected、缺省、private。具体访问范围如下:

注意:

   类:只能使用public、缺省修饰

   类的内部成员:可以使用4种权限修饰进行修饰。

开发中4种权限使用频率的情况:
   比较高:public、private
   比较低:缺省、protected

封装性的体现:

   ①私有化(private)类的属性,提供公共(public)的get和set方法,对此属性进行获取或修改

   ②将类中不需要对外暴露的方法,设置为private

   ③单例模式中构造器private的了,避免在类的外部创建实例。(放到static关键字后讲)

私有化类的成员变量,提供公共的get和set方法,对外暴露获取和修改属性的功能。
public class Person { 
   private int age;
   public void setAge(int a) { 
      age = a; 
   } 
   public int getAge() { 
      return age; 
   }
}
public class PersonTest { 
   public static void main(String[] args) { 
       p.setAge(23); 
       System.out.println("p.age = " + p.getAge());
   } 
} 

开发中,一般成员实例变量都习惯使用private修饰,再提供相应的public权限的get/set方法访问。

成员变量封装的好处:

      让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里面加入控制逻辑,

      限制对成员变量的不合理访问。还可以进行数据检查,从而有利于保证对象信息的完整

      性。

      便于修改,提高代码的可维护性。主要说的是隐藏的部分,在内部修改了,如果其对外

      可以的访问方式不变的话,外部根本感觉不到它的修改。

 (12)类的成员之三:构造器(Constructor)

我们new完对象时,所有成员变量都是默认值,如果我们需要赋别的值,需要挨个为它们再赋值,我们可以在new对象时,直接为当前对象的某个或所有成员变量直接赋值, Java给我们提供了构造器(Constructor),也称为构造方法。 

构造器的作用:

             搭配new关键字,创建类的对象

             在创建对象的同时,可以给对象的相关属性赋值

构造器的语法格式:

[修饰符] class 类名{ 
    [修饰符] 构造器名(){ 
         // 实例初始化代码 
    } 
    [修饰符] 构造器名(参数列表){ 
        // 实例初始化代码 
    } 
}

1. 构造器名必须与它所在的类名必须相同。 
2. 它没有返回值,所以不需要返回值类型,也不需要void。 
3. 构造器的修饰符只能是权限修饰符,不能被其他任何修饰。比如,不能被static、
   final、synchronized、abstract、native 修饰,不能有 return 语句返回值。
4. 创建类以后,在没有显示提供任何构造器的情况下,系统会默认提供一个空参的构造器,
   且构造器的权限与类声明的权限相同。
5. 一旦类中显示声明了构造器,则系统不再提供默认的空参的构造器。
6. 一个类中可以声明多个构造器,彼此之间构成重载。

(13) 类中属性赋值过程

在类的属性中,可以有哪些位置给属性赋值?

     ① 默认初始化  

     ② 显式初始化

     ③ 构造器中初始化

     ④ 通过"对象.属性"或"对象.方法"的方式,给属性赋值

这些位置执行的先后顺序是怎样?

      顺序:① - ② - ③ - ④

     上述中的①、②、③在对象创建过程中,只执行一次。 ④ 是在对象创建后执行的,可以

      根据需求多次执行。

(14)JavaBean和UML 类图

JavaBean:

JavaBean 是一种Java语言写成的可重用组件

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

       类是公共的

       有一个无参的公共的构造器

       有属性,且有对应的get、set方法

用户可以使用JavaBean将功能、处理、值、数据库访问和其他任何可以用Java代码创造的对象进行打包,并且其他的开发者可以通过内部的JSP页面、Servlet、其他 JavaBean、applet 程序或者应用来使用这些对象。用户可以认为JavaBean提供了一种随时随地的复制和粘贴的功能,而不用关心任何改变。

《Think in Java》中提到,JavaBean最初是为Java GUI的可视化编程实现的。你拖动IDE构建工具创建一个GUI 组件(如多选框),其实是工具给你创建Java类,并提供将类的属性暴露出来给你修改调整,将事件监听器暴露出来。

alt+insert可以自动生成get,set

示例:
public class JavaBean { 
   private String name; // 属性一般定义为private 
   private int age; 
   public JavaBean() { 
   } 
   public int getAge() { 
      return age; 
   } 
   public void setAge(int a) { 
       age = a; 
   } 
   public String getName() { 
       return name; 
   } 
   public void setName(String n) { 
       name = n; 
   } 
} 

UML 类图:

UML(Unified Modeling Language,统一建模语言),用来描述软件模型和架构的图形化语言

常用的UML工具软件有PowerDesinger、Rose和Enterprise Architect。

UML工具软件不仅可以绘制软件开发中所需的各种图表,还可以生成对应的源代码。

在软件开发中,使用UML类图可以更加直观地描述类内部结构(类的属性和操作) 以及类之间的关系(如关联、依赖、聚合等)。

        +表示 public 类型, - 表示 private 类型,#表示protected类型

         方法的写法: 方法的类型(+、-) 方法名(参数名: 参数类型):返回值类型

         斜体表示抽象方法或类。

 (15)关键字:this

我们在声明一个属性方法时,通过形参给对应的属性赋值。如果形参名和属性名同名了,那么该如何在方法内区分这两个变量呢?
使用this。具体来讲,使用this修饰的变量,表示的是属性。没有用this修饰的,表示的是

形參。

this 可以调用的结构:成员变量、方法和构造器   

this在方法(准确的说是实例方法或非static的方法)内部使用,表示调用该方法的对象

this在构造器内部使用,表示该构造器正在初始化的对象。

针对于方法内的使用情况: (准确的说是非static修饰的方法):

一般情况,我们通过对象a调用方法,可以在方法内调用当前对象a的属性或其他方法。此时,我们可以在属性和其他方法前使用"this.",表示当前属性或方法所属的对象a。但是,一般情况下,我们都选择省略此"this."结构。

特殊情况,如果方法的形参与对象的属性同名了,我们必须使用"this."进行区分。使用this.修饰的变量即为属性(或成员变量),没有使用this.修饰的变量,即为局部变量。

针对于构造器内的使用情况:

一般情况,我们通过构造器创建对象时,可以在构造器内调用当前正在创建的对象的属性或方法。此时,我们可以在属性和方法前使用"this.",表示当前属性或方法所属的对象。但是,一般情况下,我们都选择省略此"this."结构。

特殊情况,如果构造器的形参与正在创建的对象的属性同名了,我们必须使用"this."进行区分。使用this.修饰的变量即为属性(或成员变量),没有使用this.修饰的变量,即为局部变量。

this调用构造器:
     格式:this(形参列表)
     我们可以在类的构造器中,调用当前类中指定的其它构造器

     方法中不能使用this(形参列表),也就是构造器能调用方法,方法不能调用构造器
     要求:this(形参列表)必须声明在当前构造器的首行
     结论:this(形参列表)在构造器中最多声明一个

     不能出现递归调用。比如,调用自身构造器。
     如果一个类中声明了n个构造器,则最多有n-1个构造器可以声明有this(形参列表)的结构

 (16)面向对象的继承性

继承的理解:
自上而下:定义了一个类A,在定义另一个类B时,发现类B的功能与类A相似,考虑类B继承于类A   
自下而上:定义了类B,C,D等,发现B、C、D有类似的属性和方法,则可以考虑将相同的属性和方法进行抽取,封装到类A中,让类B、C、D继承于类A,同时,B、C、D中的相似的功能就可以删除了。

继承的好处:

      继承的出现减少了代码冗余,提高了代码的复用性。

      继承的出现,更有利于功能的扩展。

      继承的出现让类与类之间产生了is-a的关系,为多态的使用提供了前提。

      继承描述事物之间的所属关系,这种关系是:is-a 的关系。可见,父类更通用、更一

      般,子类更具体。 

      注意:不要仅为了获取其他类中某个功能而去继承!

继承中的语法格式:

通过 extends 关键字,可以声明一个类B继承另外一个类A,定义格式如下: 
[修饰符] class 类A { 
     ... 
} 
[修饰符] class 类B extends 类A { 
     ... 
} 

继承中的基本概念:
类B,称为子类、派生类(derived class)、SubClass 
类A,称为父类、超类、基类(base class)、SuperClass

继承性的细节说明:
      子类就获取到了父类中声明的所有的属性和方法。

      但是,由于封装性的影响,子类不能直接访问父类中私有的(private)的成员变量和方法,

      但可以用public的方法使用私有的成员变量和方法

      子类在继承父类以后,还可以扩展自己特有的功能(体现:增加特有的属性、方法)

              extends:延展、扩展、 延伸,即子类不是父类的子集,而是对父类的“扩展”
      不要为了继承而继承。在继承之前,判断一下是否有is a的关系。

默认的父类:
Java中声明的类,如果没有显式的声明其父类时,则默认继承于java.lang.0bject

 class test{
   public static void main(String[] args){
       student s=new student();
       person p=new person();
       System.out.println(s.getClass().getSuperclass());输出class person
       System.out.println(p.getClass().getSuperclass());输出class java.lang.Object
   }
}
class person {}
class student extends person {}

Java 支持多层继承(继承体系):

class A{} 
class B extends A{} 
class C extends B{} 
说明: 子类和父类是一种相对的概念 
       顶层父类是Object类。所有的类默认继承Object,作为父类。

(17)方法的重写 

何为方法的重写?
子类对父类继承过来的方法进行的覆盖、覆写的操作,就称为方法的重写。

方法重写应遵循的规则:

方法声明的格式:
     权限修饰符 返回值类型 方法名(形参列表)[throws异常类型]{
                    //方法体
     }

具体规则:
   ①父类被重写的方法与子类重写的方法的方法名和形参列表必须相同。
   ②子类重写的方法的权限修饰符不小于父类被重写的方法的权限修饰符
      子类不能重写父类中声明为private权限修饰的方法。
   ③关于返回值类型:
      父类被重写的方法的返回值类型是void,则子类重写的方法的返回值类型必须是void
      父类被重写的方法的返回值类型是基本数据类型,则子类重写的方法的返回值类型必须
      与被重写的方法的返回值类型相同
      父类被重写的方法的返回值类型是引用数据类型(比如类),则子类重写的方法的返回值
      类型可以与被重写的方法的类型相同或是被重写的方法的返回值类型的子类
   ④(超纲)子类重写的方法抛出的异常类型可以与父类被重写的方法抛出的异常类型相同,或是
     父类被重写的方法抛出的异常类型的子类
   补充说明:方法体:没有要求。但是子类重写的方法的方法体必然与父类被重写的方法的不同。

面试题:区分方法的重载(overload)与重写(override/overwrite)
   重载:在同一个类中,允许存在一个以上的同名方法,只要它们的参数列表不同即可
   重写;继承之后,子类覆盖父类中同名同参数的方法

(18)关键字:super 

在Java 类中使用super来调用父类中的指定操作:

     super可用于访问父类中定义的属性

     super可用于调用父类中定义的成员方法

     super可用于在子类构造器中调用父类的构造器

注意:

     尤其当子父类出现同名成员时,可以用super表明调用的是父类中的成员

     super的追溯不仅限于直接父类

     super和this的用法相像,this代表本类对象的引用,super代表父类的内存空间的标识

     如果子类重写了父类的方法,在子类中需要通过super.才能调用父类被重写的方法,否则

     默认调用的子类重写的方法

super调用构造器:

① 子类继承父类时,不会继承父类的构造器。只能通过“super(形参列表)”的方式调用父类指定
  的构造器。
② 规定:"super(形参列表)”,必须声明在构造器的首行。
③ 我们前面讲过,在构造器的首行可以使用"this(形参列表)",调用本类中重载的构造器,结合②,
  结论:在构造器的首行,"this(形参列表)”和"super(形参列表)"只能二选一。
④ 如果在子类构造器的首行既没有显示调用"this(形参列表)",也没有显式调用"super(形参列表)",
  则子类此构造器默认调用"super()",即调用父类中空参的构造器。
⑤ 由③和④得到结论:子类的任何一个构造器中,要么会调用本类中重载的构造器,要么会调用父类的
  构造器。只能是这两种情况之一。
⑥ 由⑤得到:一个类中声明有n个构造器,最多有n-1个构造器中使用了"this(形参列表)",则剩下
  的那个一定使用"super(形参列表)"。

  我们在通过子类的构造器创建对象时,一定在调用子类构造器的过程中,直接或间接的调用到父类
  的构造器。也正因为调用过父类的构造器,我们才会将父类中声明的属性或方法加载到内存中,供
  子类对象使用。

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

当我们通过子类的构造器创建对象时,子类的构造器一定会直接或间接的调用到其父类的构造器,
而其父类的构造器同样会直接或间接的调用到其父类的父类的构造器,....,直到调用了0bject类
中的构造器为止。
正因为我们调用过子类所有的父类的构造器,所以我们就会将父类中声明的属性、方法加载到内存中,
供子类的对象使用

在创建子类对象的过程中,一定会调用父类中的构造器

问题:创建子类的对象时,内存中到底有几个对象?
就只有一个对象!即为当前new后面构造器对应的类的对象。

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

Java中多态性的体现:
    子类对象的多态性:父类的引用指向子类的对象。(或子类的对象赋给父类的引用)

格式:父类类型 变量名 =new 子类类型( );

         (父类类型:指子类继承的父类类型,或者实现的接口类型)

多态性的应用: 虚拟方法调用
    在多态的场景下,调用方法时:
        编译时,认为方法是左边声明的父类的类型的方法(即被重写的方法)

        执行时,实际执行的是子类重写父类的方法。

        简称为:编译看左边,运行看右边。

多态性的使用前提:①要有类的继承关系

                                 ②要有方法的重写

多态的适用性:适用于方法,不适用于属性。

多态的好处:极大的减少了代码的冗余,不需要定义多个重载的方法。

多态的弊端:一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象, 那么该

                     变量就不能再访问子类比父类多出的属性和方法,只能访问重写的。

开发中:使用父类做方法的形参,是多态使用最多的场合。即使增加了新的子类,方法也无

               需改变

多态是编译时行为还是运行时行为?

              运行时  

多态性向下转型:

为什么要类型转换 
   因为多态,就一定会有把子类对象赋值给父类变量的时候,这个时候,在编译期间,就会出现
   类型转换的现象。
   但是,使用父类变量接收了子类对象之后,我们就不能调用子类拥有而父类没有的方法了。
   这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做类型转换,
   使得编译通过。

如何向下转型:
   子类类型 子类变量 = (子类类型)父类变量
   例如:Man是Person的子类
        Person p=new Man();
        Man m=(Man)p; 向下转换

 instanceof 关键字:
   为了避免ClassCastException 的发生,Java 提供了 instanceof 关键字,给引用变量做
   类型的校验。
   格式: 对象a instanceof 数据类型A   判断a是否是A的实例
   要求: 只要用instanceof 判断返回true的,那么强转为该类型就一定是安全的,不会报 
         ClassCastException 异常。 
         如果对象a属于类A的子类B,a instanceof A值也为true。 
         要求对象a所属的类与类A必须是子类和父类的关系,否则编译错误。 

(21) Object 类的概念

类 java.lang.Object 是类层次结构的根类,即所有其它类的父类。

任何一个Java类(除0bject类)都直接或间接的继承于0bject类
0bject类称为java类的根父类

0bject类中声明的结构(属性、方法等)就具有通用性。
    0bject类中没有声明属性
    0bject类提供了一个空参的构造器
    重点关注:Object类中声明的方法

equals方法:

equals子类使用说明:
   自定义的类在没有重写0bject中equals()方法的情况下,调用的就是0bject类中声明的
   equals(),比较两个对象的引用地址是否相同。(或比较两个对象是否指向了堆空间中的同
   一个对象实体)
   对于像String、File、Date和包装类等,它们都重写了0bject类中的equals()方法,用
   于比较两个对象的实体内容是否相等。原因:在这些类中重写了Object类的equals()方法。
   当自定义使用equals()时,可以重写。用于比较两个对象的“内容”是否都相等 
   
equals开发中使用说明:
   实际开发中,针对于自定义的类,常常会判断两个对象是否equals(),而此时主要是判断两
   个对象的属性值是否相等。所以:我们要重写0bject类的equals()方法。
equals如何重写:
   手动自己实现
   调用IDEA自动实现(推荐)

高频面试题:区分==和 equals()
   ==:运算符
      ①使用范围:基本数据类型、引用数据类型
      ②基本数据类型:判断数据值是否相等
      ③引用数据类型变量:比较两个引用变量的地址值是否相等。
      (或比较两个引用是否指向同一个对象实体)
   equals():方法
      使用范围:只能使用在引用数据类型上。
      如果该方法没有被重写过默认也是==,我们可以看到String等类的equals方法是被重写过的
     例如: String c="1";
           String d="1";
           System.out.println(c.equals(d)); true

toString()方法:

0bject类中toString()的定义:
public String toString{
    return getClass().getName(+"+ Integer.toHexString(hashCode());
}
默认情况下,toString()返回的是“对象的运行时类型 @ 对象的hashCode值的十六进制形式" 
在进行String与其它类型数据的连接操作时,自动调用toString()方法
平时我们在调用System.out.println()打印对象引用变量时,其实就调用了对象的toString()
例如:  person a=new person();
        System.out.println(a); person@3b07d329

子类使用说明:
  自定义的类,在没有重写0bject类的toString()的情况下,默认返回的是当前对象的地址值。
  像String、File、Date或包装类等0bject的子类,它们都重写了0bject类的toString(),
  在调用toString()时,返回当前对象的实体内容。
  例如: Date a=new Date();
         System.out.println(a);  Sun Jun 30 18:25:50 CST 2024

开发中使用说明:
  习惯上,开发中对于自定义的类在调用toString()时,也希望显示其对象的实体内容,
  而非地址值。这时候,就需要重写0bject类中的toString().|

clone()和finalize()方法:

clone() 克隆对象,重新开辟一块空间

finalize() 
当对象被回收时,系统自动调用该对象的finalize()方法(不是垃圾回收器调用的,是本类对象调用的)
永远不要主动调用某个对象的finalize方法,应该交给垃圾回收机制调用。
什么时候被回收:
    当某个对象没有任何引用时,JVM就认为这个对象是垃圾对象,就会在之后不确定的时间使用
    垃圾回收机制来销毁该对象,在销毁该对象前,会先调用finalize()方法。  
子类可以重写该方法,目的是在对象被清理之前执行必要的清理操作。比如,在方法内断开相关连
接资源。
如果重写该方法,让一个新的引用变量重新引用该对象,则会重新激活对象。 
在JDK 9中此方法已经被标记为过时的。

(22)关键字:static 

static静态的  用来修饰的结构:属性、方法;代码块、内部类;

静态变量:类中的属性使用static进行修饰。

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

语法格式

[修饰符] class 类{
    [其他修饰符] static 数据类型 变量名; 
​​​​​​​    [其他修饰符] static 返回值类型 方法名(形参列表){ 
                方法体;
    } 
}

对比静态变量与实例变量:

    ①个数

       静态变量:在内存空间中只有一份,被类的多个对象所共享。
       实例变量:类的每一个实例(或对象)都保存着一份实例变量。

    ②内存位置
        静态变量:jdk6及之前:存放在方法区。jdk7及之后:存放在堆空间
        实例变量:存放在堆空间的对象实体中。

    ③加载时机
       静态变量:随着类的加载而加载,由于类只会加载一次,所以静态变量也只有一份。
       实例变量:随着对象的创建而加载。每个对象拥有一份实例变量。

    ④调用者
        静态变量:可以被类直接调用,也可以使用对象调用。
        实例变量:只能使用对象进行调用。

    ⑤判断是否可以调用--->从生命周期的角度解释

                     静态变量     实例变量

         类          yes             no

         对象       yes            yes

     ⑥消亡时机
         静态变量:随着类的卸载而消亡
         实例变量:随着对象的消亡而消亡

     static修饰的方法内,不能使用this和super

static修饰方法:(类方法、静态方法)
      随着类的加载而加载
      可以通过“类.静态方法”的方式,直接调用静态方法
      静态方法内可以调用静态的属性(属性和方法的前缀使用的是当前类,可以省略)

      但静态的方法不可以调用非静态的结构。(比如:属性、方法)

                      类方法     实例方法

         类           yes             no

         对象        yes            yes

     补充:在类的非静态方法中,可以调用当前类中的静态结构(属性、方法)或非静态结构

             (属性、方法)

开发中,什么时候需要将属性声明为静态的?
      判断当前类的多个实例是否能共享此成员变量,且此成员变量的值是相同的。
      开发中,常将一些常量声明是静态的。比如:Math类中的PI

什么时候需要将方法声明为静态的?
      方法内操作的变量如果都是静态变量(而非实例变量)的话,则此方法建议声明为静态

      方法  
      开发中,常常将工具类中的方法,声明为静态方法。比如:Arrays类、Math类

(23) 单例设计模式与main的理解

设计模式概述:

设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。设计模式免去我们自己再思考和摸索。就像是经典的棋谱,不同的棋局,我们用不同的棋谱。"套路" 

经典的设计模式共有23种。每个设计模式均是特定环境下特定问题的处理方法。

何为单例模式:

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。

单例模式的两种实现方式:

饿汉式 
class Singleton { 
   private Singleton() { 私有化构造器 
   } 
   内部提供一个当前类的实例,此实例也必须静态化 
   private static Singleton single = new Singleton();
   public static Singleton getInstance() { 提供公共的静态的方法,返回当前类的对象 
      return single; 
   } 
}

懒汉式 
class Singleton { 
   private Singleton() { 私有化构造器 
   } 
   private static Singleton single; 内部提供一个当前类的实例,此实例也必须静态化 
   public static Singleton getInstance() {  提供公共的静态的方法,返回当前类的对象 
     if(single == null) { 
        single = new Singleton(); 
     } 
     return single; 
   } 
}

饿汉式 vs 懒汉式
  特点:
    饿汉式:“立即加载”,随着类的加载,当前的唯一实例就创建了
    懒汉式:"延迟加载",在需要使用的时候,进行创建。
  优缺点:
    饿汉式:(优点)写法简单,由于内存中较早加载,使用更方便、更快。是线程安全的。
           (缺点)内存中占用时间较长。
    懒汉式:(优点)在需要的时候进行创建,节省内存空间。
           (缺点)线程不安全(放到多线程章节时解诀)

单例模式的优点及应用场景:

由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,
如读取配置、产生其他依赖对象时,则可以通过在应用启动时 直接产生一个单例对象,然后
永久驻留内存的方式来解决。

应用场景 
• Windows的Task Manager (任务管理器)就是很典型的单例模式 
• Windows的Recycle Bin (回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护
  着仅有的一个实例。 
• Application 也是单例的典型应用 
• 应用程序的日志应用,一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,
  因为只能有一个实例去操作,否则内容不好追加。 
• 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。

理解main 方法的语法:

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

(24)类的成员之四:代码块 

如果成员变量想要初始化的值不是一个硬编码的常量值,而是需要通过复杂的计算或读取文件、或读取运行环境信息等方式才能获取的一些值,此时,可以考虑代码块(或初始化块)。

代码块(或初始化块)的作用:用来初始化类或对象的信息(即初始化类或对象的成员变量)

代码块的修饰:只能使用static进行修饰。

代码块的分类:静态代码块:使用static修饰

                         非静态代码块:没有使用static修饰

语法:

【修饰符】 class 类{ 
   static{   在代码块的前面加static,就是静态代码块。
       静态代码块 
   } 
}

静态代码块:
    随着类的加载而执行
    由于类的加载只会执行一次,进而静态代码块的执行,也只会执行一次
    作用:用来初始化类的信息
    内部可以声明变量、调用属性或方法、编写输出语句等操作。
    静态代码块的执行要先于非静态代码块的执行
    如果声明有多个静态代码块,则按照声明的先后顺序执行
    静态代码块内部只能调用静态的结构(即静态的属性、方法),不能调用非静态的结构

  (即非静态的属性、方法)

非静态代码块:
    随着对象的创建而执行
    每创建当前类的一个实例,就会执行一次非静态代码块
    作用:用来初始化对象的信息 

    内部可以声明变量、调用属性或方法、编写输出语句等操作。
    如果声明有多个非静态代码块,则按照声明的先后顺序执行
    非静态代码块内部可以调用静态的结构(静态的属性、方法),也可以调用非静态的结构

  (即非静态的属性、方法)

非静态代码块的意义:

    如果多个重载的构造器有公共代码,并且这些代码都是先于构造器其他代码执行的,那么

    可以将这部分代码抽取到非静态代码块中,减少冗余代码。

(25)类中属性赋值的位置及过程

可以给类的非静态的属性(即实例变量)赋值的位置有:
    默认初始化
    显式初始化
    构造器中初始化
    代码块中初始化  

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

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

(超纲)关于字节码文件中的<init>的简单说明:

     <init>方法在字节码文件中可以看到。每个<init>方万法都对应着一个类的构造器。(类中

     声明了几个构造器就会有几个<init>)
     编写的代码中的构造器在编译以后就会以<init>方法的方式呈现
     <init>方法内部的代码包含了实例变量的显示赋值、代码块中的赋值和构造器中的代码。
     <init>方法用来初始化当前创建的对象的信息的。

给实例变量赋值的位置很多,开发中如何选?
     显示赋值:比较适合于每个对象的属性值相同的场景
     构造器于赋值:比较适合于每个对象的属性值不相同的场景 

(26)final关键字 

final:最终的,不可更改的

final修饰类:表示此类不能被继承。比如:String、StringBuffer、StringBuilder类

final修饰方法:表示此方法不能被重写,比如:Object类中的getClass()

final修饰变量:既可以修饰成员变量,也可以修饰局部变量。此时的"变量"其实就变成了"常

                         量",意味着一旦赋值,就不可更改。

final修饰成员变量:有哪些位置可以给成员变量赋值?

                                 显式赋值  
                                 代码块中赋值

                                 构造器中赋值

final修饰局部变量:一旦赋值就不能修改

     方法内声明的局部变量:在调用局部变量前,一定需要赋值。而且一旦赋值,就不可更改

     方法的形参:在调用此方法时,给形参进行赋值。而且一旦赋值,就不可更改

final与static搭配:修饰成员变量时,此成员变量称为:全局常量。比如:Math的PI

(27)抽象类与抽象方法(或abstract关键字)

随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般, 更通用。类的设计应该保证父类和子类能够共享特征。有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类。

abstract的概念:抽象的
abstract可以用来修饰: 类、方法

语法格式:

public abstract class Animal { 
   public abstract void eat();  抽象方法没有方法体
} 
public class Cat extends Animal { 
   public void eat (){ 
      System.out.println("小猫吃鱼和猫粮");  
   } 
} 
public class CatTest { 
   public static void main(String[] args) { 
      Cat c = new Cat();  创建子类对象 
      c.eat(); 调用eat方法 
   } 
} 

abstract修饰类:
     此类称为抽象类
     抽象类不能实例化。
     抽象类中是包含构造器的,因为子类对象实例化时,需要直接或间接的调用到父类的构造

     器。
     抽象类中可以没有抽象方法。反之,抽象方法所在的类,一定是抽象类。

abstract修饰方法:
    此方法即为抽象方法
    抽象方法只有方法的声明,没有方法体。  
    抽象方法其功能是确定的(通过方法的声明即可确定),只是不知道如何具体实现(体现

    为没有方法体)
    子类必须重写父类中的所有的抽象方法之后,方可实例化。否则,此子类仍然是一个抽象

    类。

abstract 不能修饰哪些结构:属性、构造器、代码块等。

abstract 不能与哪些关键宇共用?(自洽)  

   不能用abstract修饰私有方法、静态方法、final的方法、final的类。
   私有方法不能重写
   避免静态方法使用类进行调用
   final的方法不能被重写    

(28)接口(interface)

接口的理解:接口的本质是契约、标准、规范,就像我们的法律一样。制定好后大家都要遵守。抽象类作为父类,接口侧重于行为,是对行为的抽象

定义接口的关键字:interface

接口内部结构的说明: 
    可以声明: 属性:必须使用public static final修饰

                       方法:jdk8之前:声明抽象方法,修饰为public abstract 

                                  jdk8:声明静态方法、默认方法
                                  jdk9:声明私有方法

    不可以声明:构造器、代码块等

接口与类的关系:实现关系,类实现接口,该类可以称为接口的实现类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements关键字。

格式:

[修饰符] interface 接口名{ 
   接口的成员列表:
   public static final int num=1;   public static final可省略
   公共的抽象方法:
   public abstract void fly();      public abstract也可省略
   公共的静态常量 
   公共的默认方法(JDK1.8以上):
   public default void fly(){
      System.out.println("飞");
      ①接口中声明的默认方法可以被实现类继承,实现类在没有重写此方法的情况下,
       默认调用接口中声明的默认方法。如果实现类重写了此方法,则调用的是自己重
       写的方法。
      ②如果类实现了两个接口,而两个接口中定义了同名同参数的默认方法。则实现类在没有
       重写此两个接口默认方法的情况下,会报错。---->接口冲突
       要求:此时实现类必须要重写接口中定义的同名同参数的方法。
      ③如果子类(或实现类)继承了父类并实现了接口。父类和接口中声明了同名同参数的方法。
      (其中,接口中的方法是默认方法)。默认情况下,子类(或实现类)在没有重写此方法的情
       况下,调用的是父类中的方法。--->类优先原则
      ④在子类(或实现类)中调用父类或接口中被重写的方法:
       类名.super.方法()
   }
   公共的静态方法(JDK1.8以上):
   public static void fly(){
      System.out.println("飞");
      接口中声明的静态方法只能被接口来调用,不能使用其实现类进行调用。
   }
   私有方法(JDK1.9以上) 
   private void fly(){
     System.out.println("我是接口中定义的私有方法");
   }
} 

【修饰符】 class 实现类  implements 接口{ 
    重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写 
    重写接口中默认方法【可选】 
} 
【修饰符】 class 实现类 extends 父类 implements 接口{ 
    重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写 
    重写接口中默认方法【可选】 
}


class A extends SuperA implements B,C{}
A相较于SuperA来讲,叫做子类
A相较于B,C来讲,叫做实现类。

满足此关系之后,说明:
    类可以实现多个接口。
    类针对于接口的多实现,一定程度上就弥补了类的单继承的局限性。
    类必须将实现的接口中的所有的抽象方法都重写(或实现),方可实例化。否则,此实现

    类必须声明为抽象类。

接口与接口的关系:继承关系,且可以多继承

interface AA{
   void method1();
}
interface BB{
   void method1();
}
interface CC extends AA,BB{}
class DD implements CC{
   public void methodl {}
   public void method2 {}
}

接口的多态性:接口名  变量名= new  实现类对象;

interface  a{
   void method1();
}
interface  b{
   void method2();
}
class c implements a{
    public void method1(){
        System.out.println("1");
    }
    public void method2(){
        System.out.println("2");
    }
}
class test{
    public static void main(String[] args){
        a d=new c();
        d.method1();
        b e=new c();
        e.method2();
     }
}

区分抽象类和接口:

    共性:都可以声明抽象方法,都不能实例化
    不同:抽象类一定有构造器。接口没有构造器

使用接口的静态成员:

    接口不能直接创建对象,但是可以通过接口名 直接调用接口的静态方法和静态常量。

使用接口的非静态方法:

    对于接口的静态方法,直接使用“接口名.”进行调用即可,也只能使用“接口名."进行调用,不

    能通过实现类的对象进行调用

    对于接口的抽象方法、默认方法,只能通过实现类对象才可以调用,接口不能直接创建对

    象,只能创建实现类的对象

 (29)类的成员之五:内部类

什么是内部类:

将一个类A定义在另一个类B里面,里面的  那个类A就称为内部类 (InnerClass),类B则称为外部类(OuterClass)。

为什么要声明内部类:

具体来说,当一个事物A的内部,还有一个部分需要一个完整的结构B进行描述,而这个内部的完整的结构B又只为外部事物A提供服务,不在其他地方单独使用,那么整个内部的完整结构B最好使用内部类。

总的来说,遵循高内聚、低耦合的面向对象开发原则。

内部类使用举例:
Thread类内部声明了State类,表示线程的生命周期

HashMap类中声明了Node类,表示封装的key和value  

内部类的分类:

      成员内部类:直接声明在外部类的里面。
              使用static修饰的:静态的成员内部类
              不使用static修饰的:非静态的成员内部类

      局部内部类:声明在方法内、构造器内、代码块内的内部类
              匿名的局部内部类
              非匿名的局部内部类

关于成员内部类的理解:

   从类的角度看:
       内部可以声明属性、方法、构造器、代码块、内部类等结构
       此内部类可以声明父类,可以实现接口

       可以使用final修饰
       可以使用abstract修饰

   从外部类的成员的角度看:
       在内部可以调用外部类的结构。比如:属性、方法等
       除了使用public、缺省权限修饰之外,还可以使用private、protected修饰

       可以使用static修饰

成员内部类的使用:

[修饰符] class 外部类{ 
   [其他修饰符] [static] class 内部类{} 
} 

创建成员内部类对象:
    实例化静态内部类:外部类名.静态内部类名 变量 =new 外部类名.静态内部类名();
    实例化非静态内部类:外部类名 变量1 = new 外部类(); 
                      外部类名.非静态内部类名 变量2 = 变量1.new 非静态内部类名();

举例:
class test{
    public static void main(String[] args){
       person.man theman=new person.man();  创建person的静态成员内部类的实例
       theman.eat();
       person p1=new person();  
       person.woman thewoman=p1.new woman();  创建person的非静态的成员内部类的实例
       thewoman.drink();
     }
}
class person{
    int num=10;
    static  class man{
       public  void eat(){
        System.out.println("吃");
       }
    }
    class woman{
       int num=20;
       public void drink(){
           System.out.println("喝");
           System.out.println(this.num); 调用本类的结构,this可省略
           System.out.println(person.this.num); 在成员内部类中调用外部类的结构,
           如果本类中属性或方法没有与外部类重名的话不用加person.this
       }
    }
}

局部内部类的使用:

非匿名局部内部类:
语法格式: 
[修饰符] class 外部类{ 
   [修饰符] 返回值类型  方法名(形参列表){ 
      [final/abstract] class 内部类{} 
   }     
} 

匿名内部类:
new 父类([实参列表]){ 
   重写方法... 
} 
new 父接口(){ 
   重写方法... 
} 

举例1:使用匿名内部类的对象直接调用方法: 
interface A{ 
   void a(); 
} 
public class Test{ 
   public static void main(String[] args){ 
       new A(){ 
          @Override 
          public void a() { 
             System.out.println("aaaa"); 
          } 
       }.a(); 
   } 
} 

举例2:通过父类或父接口的变量多态引用匿名内部类的对象 
interface A{ 
   void a(); 
} 
public class Test{ 
   public static void main(String[] args){ 
      A obj = new A(){ 
         @Override 
         public void a() { 
           System.out.println("aaaa"); 
         } 
      }; 
      obj.a(); 
   } 
}

举例3:匿名内部类的对象作为实参 
interface A{ 
   void method(); 
} 
public class Test{ 
   public static void test(A a){ 
      a.method(); 
   } 
   public static void main(String[] args){ 
      test(new A(){ 
         @Override 
         public void method() { 
            System.out.println("aaaa"); 
         } 
      }); 
   }    
} 

 (30)枚举类

枚举类的理解:
枚举类型本质上也是一种类,只不过是这个类的对象是有限的、固定的几个,不能让用户随意创建。

举例:

   星期:Monday(星期一)......Sunday(星期天)

   性别:Man(男)、Woman(女)

   月份:January(1月)......December(12 月)

   季节:Spring(春节)......Winter(冬天) 

开发中的建议:
   开发中,如果针对于某个类,其实例是确定个数的。则推荐将此类声明为枚举类。

   若枚举只有一个对象, 则可以作为一种单例模式的实现方式。

JDK5.0之前如何自定义枚举类(了解):

class SeasonTest{
    public static void main(String[] args){
        System.out.println(Season.AUTUMN);
     }
}
class Season{
   //声明当前类的对象的实例变量,使用private final修饰
   private final String SEASONNAME;//季节的名称
   private final String SEASONDESC;//季节的描述
   private Season(String seasonName,String seasonDesc){  //私有化类的构造器
      this.SEASONNAME = seasonName;
      this.SEASONDESC = seasonDesc;
   }
   //创建当前类的实例,需要使用public static final修饰
   public static final Season SPRING = new Season("春天", "春暖花开");
   public static final Season SUMMER = new Season("夏天", "夏日炎炎");
   public static final Season AUTUMN = new Season("秋天", "秋高气爽");
   public static final Season WINTER = new Season("冬天", "白雪皑皑");
   public String toString() {
      return "Season{" + "SEASONNAME='" + SEASONNAME + '\'' + ", SEASONDESC='"
      + SEASONDESC + '\'' + '}';
   }
}

定义枚举类(JDK5.0 之后)

enum 关键字声明枚举:
【修饰符】 enum 枚举类名{ 
   常量对象列表 
   对象的实例变量列表; 
} 

注意:
  使用enum关键字定义的枚举类,默认其父类是java.lang.Enum类
  使用enum关键字定义的枚举类,不要再显示的定义其父类。否则报错。

举例:
class SeasonTest{
    public static void main(String[] args){
        System.out.println(Season.SPRING.getClass());
        打印class Season
        System.out.println(Season.SPRING.getClass().getSuperclass());       
        打印class java.lang.Enum
        System.out.println(Season.SPRING.getClass().getSuperclass().getSuperclass());
        打印class java.lang.Object

        //toString():默认返回的是常量名(对象名),可以继续手动重写该方法!
        System.out.println(Season.SPRING.toString());

        //name():得到当前枚举常量的名称。建议优先使用toString()
        System.out.println(Season.SPRING.name());

        //values():返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值,
          是一个静态方法
        Season[] values= Season.values();
        for (int i=0;i<values.length;i++){
           System.out.println(values[i]);
         }

        //value0f(String objName):返回当前枚举类中名称为objName的枚举类对象。
        //如果枚举类中不存在objName名称的对象,则报错。
        String objname="WINTER";
        Season season1=Season.valueOf(objname);
        System.out.println(season1);

        //ordinal():返回当前枚举常量的次序号,默认从0开始
        System.out.println(Season.AUTUMN.ordinal());
     }
}
public enum  Season   {
    //必须在枚举类的开头声明多个对象。对象之间使用,隔开
    SPRING("春天","春风又绿江南岸"),
    SUMMER("夏天","映日荷花别样红"),
    AUTUMN("秋天","秋水共长天一色"),
    WINTER("冬天","窗含西岭千秋雪");
    //声明当前类的对象的实例变量,使用private final修饰
    private final String seasonName;
    private final String seasonDesc;
    //私有化类的构造器
    private Season(String seasonName, String seasonDesc) {
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }
    public  String getSeasonName() {
        return seasonName;
    }
    public String getSeasonDesc() {
        return seasonDesc;
    }
}

使用enum关键宇定义的枚举类,默认其父类是java.lang.Enum类
使用enum关键字定义的枚举类,不要再显示的定义其父类。否则报错

枚举类实现接口的操作:

情况1:枚举类实现接口,在枚举类中重写接口中的抽象方法。当通过不同的枚举类对象调用此
      方法时,执行的是同一个方法。
情况2:让枚举类的每一个对象重写接口中的抽象方法。当通过不同的枚举类对象调用此方法时,
      执行的是不同的方法。

情况1:
class SeasonTest{
    public static void main(String[] args){
      Season.SPRING.show();
    }
}
public interface Info {
    public void show();
}
public enum  Season implements Info {
    SPRING("春天","春风又绿江南岸"),
    SUMMER("夏天","映日荷花别样红"),
    AUTUMN("秋天","秋水共长天一色"),
    WINTER("冬天","窗含西岭千秋雪");
    private final String seasonName;
    private final String seasonDesc;
    private Season(String seasonName, String seasonDesc) {
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }
    public void show(){
        System.out.println("同一个");
    }
}

情况2:
class SeasonTest{
    public static void main(String[] args){
      Season.SPRING.show();
      Season.WINTER.show();
    }
}
public interface Info {
    public void show();
}
public enum  Season implements Info {
    SPRING("春天","春风又绿江南岸"){
        public void show(){
            System.out.println("春天");
        }
    },
    SUMMER("夏天","映日荷花别样红"){
        public void show(){
            System.out.println("夏天");
        }
    },
    AUTUMN("秋天","秋水共长天一色"){
        public void show(){
            System.out.println("秋天");
        }
    },
    WINTER("冬天","窗含西岭千秋雪"){
        public void show(){
            System.out.println("冬天");
        }
    };
    private final String seasonName;
    private final String seasonDesc;
    private Season(String seasonName, String seasonDesc) {
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }
}

(31) Annotation注解、单元测试的使用

Annotation的理解:
   注解(Annotation)是从JDK5.0开始引入,以"@注解名"在代码中存在。
   Annotation可以像修饰符一样被使用,可用于修饰包、类、构造器、方法、成员变量、参

   数、局部变量的声明。还可以添加一些参数值,这些信息被保存在Annotation 的

   “name=value”对中。
   注解可以在类编译、运行时进行加载,体现不同的功能。

注解与注释:

注解也可以看做是一种注释,通过使用 Annotation,程序员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充信息。但是,注解,不同于单行注释      和多行注释。

对于单行注释和多行注释是给程序员看的。

而注解是可以被编译器或其他程序读取的。程序还可以根据注解的不同,做出相应的处理。

注解的应用场景:
    生成文档相关的注解
    在编译时进行格式检查(JDK内置的三个基本注解)

    跟踪代码依赖性,实现替代配置文件功能

Java基础涉及到的三个常用注解
   @Override:限定重写父类方法,该注解只能用于方法
   @Deprecated:用于表示所修饰的元素(类,方法等)已过时。通常是因为所修饰的结构

    危险或存在更好的选择
   @SuppressWarnings:抑制编译器警告

自定义注解:
   以@SuppressWarnings为参照,进行定义即可。
元注解的理解:
   元注解:对现有的注解进行解释说明的注解

   (1)@Target:用于描述注解的使用范围

        可以通过枚举类型ELementType的10个常量对象来指定TYPE,METHOD,

        CONSTRUCTOR,PACKAGE.
   (2)@Retention:用于描述注解的生命周期

       可以通过枚举类型RetentionPolicy的3个常量对象来指定SOURCE(源代码)、CLASS

     (字节码)、RUNTIME(运行时)唯有RUNTIME阶段才能被反射读取到。

 (3)@Documented:表明这个注解应该被javadoc工具记录。
 (4)@Inherited:允许子类继承父类中的注解


JUnit 单元测试:

测试分类:

     黑盒测试:不需要写代码,给输入值,看程序是否能够输出期望的值。

     白盒测试:需要写代码的。关注程序具体的执行流程。

JUnit 单元测试介绍:

     JUnit 测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何完成功能

     和完成什么样(What)的功能。

引入JUnit.jar:

     在需要测试的方法上面写@Test,会出现红色小灯泡,鼠标移至灯泡将其添加到类路径,

     然后左边出现绿色三角就能测试了 

要想能正确的编写单元测试方法,需要满足:

    所的类必须是public的,非抽象的,包含唯一的无参构造器。
   @Test标记的方法本身必须是public,非抽象的,非静态的,void无返回值,()无参数的。

(32)包装类 

为什么要使用包装类?
为了使得基本数据类型的变量具备引用数据类型变量的相关特征(比如:封装性、继承性、多态性),我们给各个基本数据类型的变量都提供了对应的包装类。

基本数据类型对应的包装类类型:

为什么需要转换?
一方面,在有些场景下,需要使用基本数据类型对应的包装类的对象。此时就需要将基本数据类型的变量转换为包装类的对象。比如:ArrayList的add(0bject obj);0bject类的equals(0bject obj)

对于包装类来讲,既然我们使用的是对象,那么对象是不能进行+-*/等运算的。为了能够进行这些运算,就需要将包装类的对象转换为基本数据类型的变量。

如何转换:   

(装箱)基本数据类型--->包装类:
     ①使用包装类的构造器②(建议)调用包装类的value0f(xxxxx)
(拆箱)包装类--->基本数据类型:
     调用包装类的xxxValue()
注意:原来使用基本数据类型变量的位置,改成包装类以后,对于成员变量来说,其默认值变化了!
jdk5.新特性:自动装箱、自动拆箱。

装箱:
Integer obj1 = new Integer(4);//使用构造函数函数 
Float f = new Float(“4.56”); 
Long l = new Long(“asdf”);  //NumberFormatException 
Integer obj2 = Integer.valueOf(4);//使用包装类中的valueOf方法
拆箱:
Integer obj = new Integer(4); 
int num1 = obj.intValue(); 

自动装箱与拆箱:
Integer i = 4;  自动装箱。相当于Integer i = Integer.valueOf(4); 
i = i + 5;  等号右边:将i对象转成基本数值(自动拆箱) i.intValue() + 5; 
加法运算完成后,再次装箱,把基本数值转成对象。 
注意:只能与自己对应的类型之间才能实现自动装箱与拆箱。 
Integer i = 1; 
Double d = 1;//错误的,1是int类型

String与基本数据类型、包装类之间的转换:
基本数据类型、包装类---> String类型:①调用String的重载的静态方法value0f(xxxxx);
                                   ②基本数据类型的变量+""
                                   int a = 10;   String str = String.valueOf(a); 
                                   int a = 10;   String str = a + "";
String类型--->基本数据类型、包装类:调用包装类的静态方法:parseXxx()
                                   int a = Integer.parseInt("整数的字符串"); 
                                   double d = Double.parseDouble("小数的字符串"); 

包装类缓存对象:

包装类     缓存对象 
Byte      -128~127
Short     -128~127
Integer   -128~127
Long      -128~127 
Float       没有 
Double      没有 
Character  0~127 
Boolean   true和false

Integer a = 1; 
Integer b = 1; 
System.out.println(a == b);//true 

Integer i = 128; 
Integer j = 128; 
System.out.println(i == j);//false

Integer m = new Integer(1);//新 new 的在堆中 
Integer n = 1;//这个用的是缓冲的常量对象,在方法区 
System.out.println(m == n);//false  

Integer x = new Integer(1);//新 new 的在堆中 
Integer y = new Integer(1);//另一个新new的在堆中 
System.out.println(x == y);//false

Double d1 = 1.0; 
Double d2 = 1.0; 
System.out.println(d1==d2);//false 比较地址,没有缓存对象,每一个都是新new 的

Integer i = 1000; 
double j = 1000; 
System.out.println(i==j);//true  会先将i自动拆箱为int,然后根据基本数据类型
                                 //“自动类型转换”规则,转为double比较 

Integer i = 1000; 
int j = 1000; 
System.out.println(i==j);//true 会自动拆箱,按照基本数据类型进行比较 

Integer i = 1; 
Double d = 1.0 
System.out.println(i==d);//编译报错,类型不同

 12. 异常处理

(1)异常的概述与常见异常的举例

什么是异常?
指的是程序在执行过程中,出现的非正常情况,如果不处理最终会导致JVM的非正常停止。

异常指的并不是语法错误和逻辑错误。语法错了,编译不通过,不会产生字节码文件,根本不能运行。 代码逻辑错误,只是没有得到想要的结果,例如:求a与b的和,写成了a-b

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

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

Java 异常体系:

java.lang.Throwable:异常体系的根父类

Throwable 可分为两类:Error和Exception,分别对应着java.lang.Error与java.lang.Exception 两个类。 

java.lang.Error:错误,Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况。一般不编写针对性的代码进行处理。例如:StackOverflowError(栈内存溢出)和OutOfMemoryError(堆内存溢出,简称 OOM)

java.lang.Exception:

​​​​​​​ 异常。我们可以编写针对性的代码进行处理。

 例如:空指针访问
            试图读取不存在的文件
            网络连接中断
            数组角标越界

java.lang.Exception分为编译时异常和运行时异常:

Java 程序的执行分为编译时过程和运行时过程。有的错误只有在运行时才会发生。比如:除数为0,数组下标越界等。

编译时异常:(受检异常)在执行javac.exe命令时,出现的异常。

运行时异常:(非受检异常)在执行java.exe命令时,出现的异常。如数组索引越界异常,                             编译阶段不报错,是程序运行时出现的。

(2)异常处理方式 

Java 异常处理: Java 采用的异常处理机制,是将异常处理的程序代码集中在一起,与正常的程序   代码分开,使得程序简洁、优雅,并易于维护。

方式一:

try-catch-finally

过程1:"抛"
  程序在执行的过程当中,一旦出现异常,就会在出现异常的代码处,生成对应异常类的对象,
  并将此对象抛出。一旦抛出,此程序就不执行其后的代码了。
过程2:"抓"
  针对于过程1中抛出的异常对象,进行捕获处理。此捕获处理的过程,就称为抓。一旦将异
  常进行了处理,代码就可以继续执行。

基本结构:
  try{ 
    ...... //可能产生异常的代码 
  }catch(异常类型1 e ){ 
    ...... //当产生异常类型1型异常时的处置措施 
  }catch(异常类型2 e ){ 
    ......  //当产生异常类型2型异常时的处置措施 
  }finally{ 
    ...... //无论是否发生异常,都无条件执行的语句 
  } 
 
使用细节:
  1️⃣catch分支,分为两个部分,catch()中编写异常类型和异常参数名,{}中编写如果发生
  了这个异常,要做什么处理的代码。
  2️⃣如果明确知道产生的是何种异常,可以用该异常类作为catch的参数;也可以用其父
  类作为catch的参数。
  比如:可以用ArithmeticException 类作为参数的地方,就可以用RuntimeException
        类作为参数,或者用所有异常的父类Exception类作为参数。但不能是与
        ArithmeticException 类无关的异常,如 NullPointerException(catch
        中的语句将不会执行)。
  3️⃣将可能出现异常的代码声明在try语句中。一旦代码出现异常,就会自动生成一个对应异常类的
  对象。并将此对象抛出。
  4️⃣针对于try中抛出的异常类的对象,使用之后的catch语句进行匹配。一旦匹配上,就进入catch
  语句块进行处理。一旦处理接触,代码就可继续向下执行。
  5️⃣如果声明了多个catch结构,不同的异常类型在不存在子父类关系的情况下,谁声明在上面,谁
  声明在下面都可以。如果多个异常类型满足子父类的关系,则必须将子类声明在父类结构的上面。
  否则,报错。
  6️⃣try中声明的变量,出了try结构之后,就不可以进行调用了。

catch中异常处理的方式:
  ①自己编写输出的语句。
  ②printStackTrace():打印异常的详细信息。(推荐)
  ③getMessage():获取发生异常的原因,返回字符串 

finaly的理解:
  我们将一定要被执行的代码声明在finally结构中。
  更深刻的理解。无论try中或catch中是否存在仍未被处理的异常,无论try中或catch
  中是否存在return语句等,finally中声明的语句都一定要被执行。
  finally语句和catch语句是可选的,但finally不能单独使用。

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

开发体会:
  对于运行时异常:
     开发中,通常就不进行显示的处理了。
     一旦在程序执行中,出现了运行时异常,那么就根据异常的提示信息修改代码即可。
  对于编译时异常:
     一定要进行处理。否则编译不通过。

例如:
        String friends[] = { "lisa", "bily", "kessy" };
        try {
            for (int i = 0; i < 5; i++) {
                System.out.println(friends[i]);
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            e.printStackTrace(); //打印异常的详细信息
            System.out.println(e.getMessage()); //获取发生异常的原因
        }
        System.out.println("\nthis is the end");

​​​​方式二:

格式:在方法的声明除,使用"throws 异常类型1,异常类型2,..."

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

throws 后面也可以写运行时异常类型,只是运行时异常类型,写或不写对于编
译器和程序执行来说都没有任何区别。如果写了,唯一的区别就是调用者调用
该方法后,使用try...catch 结构时,IDEA可以获得更多的信息,需要添加哪种
catch 分支。 

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

方法的重写的要求:(针对于编译时异常来说的)
子类重写的方法抛出的异常类型可以与父类被重写的方法抛出的异常类型相同,或是父类被
重写的方法抛出的异常类型的子类。
如果父类被重写方法的方法签名后面没有 “throws 编译时异常类型”,那么重写方法
时,方法签名后面也不能出现“throws 编译时异常类型”。

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

使用throw收到抛出异常:

为什么需要手动抛出异常对象?
  在实际开发中,如果出现不满足具体场景的代码问题,我们就有必要手动抛出一个指定类型的
  异常对象。

如何理解自动vs手动抛出异常对象?
  过程1:抛
       "自动抛":程序在执行的过程当中,一旦出现异常,就会在出现异常的代码处,自动生成
                对应异常类的对象,并将此对象抛出,一旦抛出,此程序就不执行其后的代码了
       "手动抛": 程序在执行的过程当中,不满足指定条件的情况下,我们主动的使用"throw +
                异常类的对象"
  过程2:抓
       狭义上讲:try-catch的方式捕获异常,并处理。
       广义上讲:把“抓”理解为“处理”。则此时对应着异常处理的两种方式:
                ① try-catch-finally ② throws

如何实现手动抛出异常?
  在方法内部,满足指定条件的情况下,使用"throw 异常类的对象"的方式抛出。
  throw new 异常类名(参数)
  注意点:throw后的代码不能被执行,编译不通过。

 (3)自定义异常类 :

如何自定义异常类?
  ①继承于现有的异常体系。通常继承于RuntimeException\Exception
  ②通常提供几个重载的构造器
  ③提供一个全局常量,声明为:static final long serialVersionUID;

如何使用自定义异常类?
  在具体的代码中,满足指定条件的情况下,需要手动的使用"throw +自定义异常类的对象"方式,
  将异常对象抛出。
  如果自定义异常类是非运行时异常,则必须考虑如何处理此异常类的对象。
 (具体的:① try-catch-finally ② throws)
举例:
  class MyException extends Exception { 
     static final long serialVersionUID = 23423423435L; 
     private int idnumber; 
     public MyException(String message, int id) { 
        super(message); 
        this.idnumber = id; 
     } 
     public int getId() { 
        return idnumber; 
     } 
  } 

为什么需要自定义异常类?
  我们其实更关心的是,通过异常的名称就能直接判断此异常出现的原因。尽然如此,我们就有必要
  在实际开发场景中,不满足我们指定的条件时,指明我们自己特有的异常类。通过此异常类的名称,
  就能判断出具体出现的问题。

注意:
  1️⃣自定义的异常只能通过throw抛出。 
  2️⃣自定义异常最重要的是异常类的名字和message属性。当异常出现时,可以根据名字
     判断异常类型。比如:TeamException("成员已满,无法添加");、 
     TeamException("该员工已是某团队成员"); 
  3️⃣自定义异常对象只能手动抛出。抛出后由try..catch处理,也可以甩锅throws给调用
    者处理。

13. 多线程 

(1).程序、进程、线程与并行、并发的概念:

程序(program):为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。

进程(process):程序的一次执行过程,或是正在内存中运行的应用程序。如:运行中的QQ,运行中的网易音乐播放器。

    1️⃣每个进程都有一个独立的内存空间,系统运行一个程序即是一个进程从创建,运行到消

         亡的过程。(生命周期)

    2️⃣程序是静态的,进程是动态的

    3️⃣进程作为操作系统调度和分配资源的最小单位(亦是系统运行程序的基本单位),系

         统在运行时会为每个进程分配不同的内存区域。

线程(thread):进程可进一步细化为线程,是程序内部的一条执行路径。一个进程中至少有一个线程。

    1️⃣一个进程同一时间若并行执行多个线程,就是支持多线程的。

    2️⃣线程作为CPU调度和执行的最小单位。

    3️⃣一个进程中的多个线程共享相同的内存单元,它们从同一个堆中分配对象,可以访问

         相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资

         源可能就会带来安全的隐患。 

    4️⃣下图中,红框的蓝色区域为线程独享,黄色区域为线程共享。

     注意: 不同的进程之间是不共享内存的。

                 进程之间的数据交换和通信 的成本很高。

线程调度:

    分时调度: 所有线程轮流使用 CPU 的使用权,并且平均分配每个线程占用 CPU 的时间。

    抢占式调度: 让优先级高的线程以较大的概率优先使用 CPU。如果线程的优先级相同,那

                        么会随机选择一个(线程随机性),Java使用的为抢占式调度。

多线程程序的优点:

   1️⃣提高应用程序的响应。对图形化界面更有意义,可增强用户体验。

   2️⃣提高计算机系统CPU的利用率

   3️⃣改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

单核CPU和多核CPU:

    单核CPU,在一个时间单元内,只能执行一个线程的任务。

多核的效率是单核的倍数吗?譬如4核A53的cpu,性能是单核A53的4倍吗?

    理论上是,但是实际不可能,至少有两方面的损耗。

    1️⃣一个是多个核心的其他共用资源限制。譬如,4核CPU对应的内存、cache、寄存器并

        没有同步扩充4倍。这就好像医院一样,1个医生换4个医生,但是做B超检查的还是一

        台机器,性能瓶颈就从医生转到B超检查了。

    2️⃣另一个是多核CPU之间的协调管理损耗。譬如多个核心同时运行两个相关的任务, 需

         要考虑任务同步,这也需要消耗额外性能。好比公司工作,一个人的时候至少不用开

         会浪费时间,自己跟自己商量就行了。两个人就要开会同步工作,协调分配,所以工

         作效率绝对不可能达到2倍。

并行与并发:

    并行(parallel):指两个或多个事件在同一时刻发生(同时发生)。指在同一时刻, 有

                                  多条指令在多个CPU上同时执行。比如:多个人同时做不同的事。

    并发(concurrency):指两个或多个事件在同一个时间段内发生。即在一段时间内,有

                                      多条指令在单个CPU上快速轮换、交替执行,使得在宏观上具有多

                                      个进程同时执行的效果。

在操作系统中,启动了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单核 CPU 系统中,每一时刻只能有一个程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

而在多核 CPU 系统中,则这些可以并发执行的程序便可以分配到多个CPU 上,实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。

(2) 线程的创建方式:

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

Thread类的特性:

    1️⃣ 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,因此把run()方法体

          称为线程执行体。

    2️⃣通过该Thread对象的start()方法来启动这个线程,而非直接调用run()

    3️⃣要想实现多线程,必须在主线程中创建新的线程对象。

方式1:继承Thread类

Java 通过继承Thread类来创建并启动多线程的步骤如下: 
   1️⃣定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要
      完成的任务 
   2️⃣重写Thread类的run()将此线程要执行的操作,声明在此方法体中
   3️⃣创建Thread子类的实例,即创建了线程对象 
   4️⃣调用线程对象的start()方法来启动该线程,
      start()的执行过程:1.启动线程 2.调用当前线程的run()

例如:创建一个分线程,用于遍历100以内的偶数
class EvenNumberTest {
    public static void main(String[] args) {
        //创建当前Thread的子类的对象
        PrintNumber t1=new PrintNumber();
        PrintNumber t2=new PrintNumber();
        //通过对象调用start()
        t1.start(); //调用第一个线程
        t2.start(); //调用第二个线程
        //能否使用t1.run()替换t1.start()的调用,实现分线程的创建和调用。不能
        //main()所在的线程执行的操作:
        for (int i=0;i<=100;i++){
            if(i%2==0){
                System.out.println(Thread.currentThread().getName()+i);
                //Thread.currentThread().getName()获取当前线程的名字
            }
        }
    }
}
class PrintNumber extends Thread{
    //重写Thread类的run()--->将此线程要执行的操作,声明在此方法体中
    @Override
    public void run() {
       for (int i=0;i<=100;i++){
           if(i%2==0){
               System.out.println(Thread.currentThread().getName()+i);
           }
       }
    }
}

方式2:创建Thread类的匿名子类的匿名对象:
  new Thread(){
        public void run() {
            for (int i = 0; i <= 100; i++) {
                if (i % 2 == 0) {
                    System.out.println(Thread.currentThread().getName() + i);
                }
            }
        }
  }.start();

方式2:实现Runnable实现

线程的创建方式二:实现Runnable接口
  1️⃣创建一个实现Runnable接口的类
  2️⃣实现接口中的run()-->将此线程要执行的操作,声明在此方法体中
  3️⃣创建当前实现类的对象
  4️⃣将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
  5️⃣Thread类的实例调用start():

例如:
//创建一个实现Runnable接口的类
class EvenNumberPrint implements  Runnable{
    //实现接口中的run()-->将此线程要执行的操作,声明在此方法体中
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+i);
        }
    }
}
class EvenNumberTest {
    public static void main(String[] args) {
        //创建当前实现类的对象
        EvenNumberPrint p=new EvenNumberPrint();
        //将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
        Thread t1= new Thread(p);
        t1.start();
        ///main()方法对应的主线程执行的操作:
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+i);
        }
        //再创建一个线程
        Thread t2= new Thread(p);
        t2.start();
    }
}

方式2:使用实现Runnable接口的方式:(提供了Runnable接口匿名实现类的匿名对象)
class EvenNumberTest {
    public static void main(String[] args) {
         new Thread(new Runnable() {
             @Override
             public void run() {
                 for (int i = 0; i < 100; i++) {
                     System.out.println(Thread.currentThread().getName()+i);
                 }
             }
         }).start();
    }
}

对比两种方式:
    共同点:①启动线程,使用的都是Thread类中定义的start()
                  ②创建的线程对象,都是Thread类或其子类的实例。

    不同点:一个是类的继承,一个是接口的实现。
    建议:建议使用实现Runnable接口的方式。
    Runnable方式的好处:①实现的方式,避免了类的单继承的局限性

                                          ②更适合处理有共享数据的问题。
                                          ③实现了代码和数据的分离。

 (3)Thread类的常用方法和生命周期:

线程中的构造器:

public Thread()分配一个新的线程对象。 
public Thread(String name)分配一个指定名字的新的线程对象。 
public Thread(Runnable target)指定创建线程的目标对象,它实现了Runnable接口的run方法 
public Thread(Runnable target,String name)分配一个带有指定目标新的线程对象并指定名字。

 线程中的常用方法:

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

过时方法:
  stop():强行结束一个线程的执行,一旦执行,线程就结束了,导致run()有未执行结束的代码。
            stop()会导致释放同步监视器,导致线程安全问题。直接进入死亡状态。不建议使用
  suspend()/ resume():可能造成死锁,所以也不建议使用

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

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

多线程的生命周期:

   JDK1.5 之前:5 种状态:

   线程的生命周期有五种状态:新建(New)、就绪(Runnable)、运行 (Running)、阻     塞(Blocked)、死亡(Dead)。CPU需要在多条线程之间切换,于是线程状态会多次在

   运行、阻塞、就绪之间切换。

   JDK1.5 及之后:6种状态 

(4)线程安全问题及解决 

当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条 记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题。但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。

同一个资源问题和线程安全问题:

   什么原因导致的?

        线程1操作xxx的过程中,尚未结束的情况下,其他线程也参与进来,对xxx进行操作。

   如何解决?

        必须保证一个线程1在操作xxx的过程中,其它线程必须等待,直到线程1操作xxx结束以

        后,其它线程才可以进行继续操作xxx

  Java是如何解决线程的安全问题的?

        使用线程的同步机制。

方式1:同步代码块

synchronized(同步监视器){
    //需要被同步的代码
}

说明:
  需要被同步的代码,即为操作共享数据的代码。
  共享数据:即多个线程都需要操作的数据。
  需要被同步的代码,在被synchronized包裹以后,就使得一个线程在操作这些代码的过程
  中,其它线程必须等待。
  同步监视器,俗称锁。哪个线程获取了锁,哪个线程就能执行需要被同步的代码。
  同步监视器,可以使用任何一个类的对象充当,该必须是惟一的。但是,多个线程必须共用同一
  个同步监视器。
注意:
  在实现Runnable接口的方式中,同步监视器可以考虑使用:this
  在继承Thread类的方式中,同步监视器要慎用this,可以考虑使用当前类.class.

案例: 
火车站要卖票,我们模拟火车站的卖票过程。因为疫情期间,本次列车的座位共100个
(即,只能出售100张火车票)。我们来模拟车站的售票窗口,实现多个窗口同时售票
的过程。注意:不能出现错票、重票。

实现Runnable接口方式:
class  SaleTicket implements  Runnable{
    int ticket=100;
    Object obj=new Object();
    public void run(){
        while (true){
            synchronized (obj){ //obj必须是唯一的
                if(ticket>0){
                    try{
                        Thread.sleep(10);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"售票号"+ticket);
                    ticket--;
                }else {
                    break;
                }
            }
        }
    }
}
class WindowTest{
    public static void main(String[] args) {
        SaleTicket s=new SaleTicket();
        Thread t1=new Thread(s);
        Thread t2=new Thread(s);
        Thread t3=new Thread(s);
        t1.start();
        t2.start();
        t3.start();
    }
}

继承Thread类方式:
class  SaleTicket extends Thread{
    static int  ticket=100;
    static Object obj=new Object();
    public void run(){
        while (true){
            synchronized (obj) {  //obj必须是唯一的
                if(ticket>0){
                    try{
                        Thread.sleep(10);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"售票号"+ticket);
                    ticket--;
                }else {
                    break;
                }
            }
        }
    }
}
class WindowTest{
    public static void main(String[] args) {
        SaleTicket s1=new SaleTicket();
        SaleTicket s2=new SaleTicket();
        SaleTicket s3=new SaleTicket();
        s1.start();
        s2.start();
        s3.start();
    }
}

同步方法:

如果操作共享数据的代码完整的声明在了一个方法中,那么我们就可以将此方法声明为同步方法即可。
synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能进入这个方法,其他线程在外面
等着。
非静态的同步方法,默认同步监视器是this
静态的同步方法,默认同步监视器是当前类本身。

继承Thread类方式:
class  SaleTicket extends Thread{
    static int  ticket=100;
    static Object obj=new Object();
    static  boolean isFlage=true;
    public void run(){
        while (isFlage){
             //obj必须是唯一的
            show();
        }
    }
    //此时同步监视器为当前类本身,即为WindowTest.class本身,是唯一的
    public static synchronized void show(){
        if(ticket>0){
            try{
                Thread.sleep(10);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"售票号"+ticket);
            ticket--;
        }else {
            isFlage=false;
        }
    }
}
public class WindowTest{
    public static void main(String[] args) {
        SaleTicket s1=new SaleTicket();
        SaleTicket s2=new SaleTicket();
        SaleTicket s3=new SaleTicket();
        s1.start();
        s2.start();
        s3.start();
    }
}

实现Runnable接口方式:
class  SaleTicket implements  Runnable{
    int ticket=100;
    Object obj=new Object();
    boolean isFlage=true;
    public void run(){
        while (isFlage){
            show();
        }
    }
    //此时的同步监视器时this,此题目中的是s,是唯一的
    public synchronized void  show(){
        if(ticket>0){
            try{
                Thread.sleep(10);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"售票号"+ticket);
            ticket--;
        }else {
            isFlage=false;
        }
    }
}
class WindowTest{
    public static void main(String[] args) {
        SaleTicket s=new SaleTicket();
        Thread t1=new Thread(s);
        Thread t2=new Thread(s);
        Thread t3=new Thread(s);
        t1.start();
        t2.start();
        t3.start();
    }
}

synchronized好处:

      解决了线程的安全问题。

弊端:

      在操作共享数据时,多线程其实是串行执行的,意味着性能低。

当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法?
      需要看其他方法是否使用synchronized修饰,同步监视器的this是否是同一个。

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

(5)线程安全的懒汉式与死锁

饿汉式没有线程安全问题,在类初始化时就直接创建单例对象,而类初始化过程是没有线程安全问题

线程安全的懒汉式:

1. 使用同步方法:
class Bank {
    private Bank() {}
    private static Bank bank = null;
    public synchronized static Bank getBank() { //同步监视器,默认为Bank.class
        if (bank == null) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            bank = new Bank();
        }
        return bank;
    }
}
public class WindowTest {
    static Bank b1 = null;
    static Bank b2 = null;
    public static void main(String[] args) {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                b1 = Bank.getBank();
            }
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                b2 = Bank.getBank();
            }
        };
        t1.start();
        t2.start();
        try {
            t1.join();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        try {
            t2.join();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(b1);
        System.out.println(b2);
        System.out.println(b1==b2);
    }
}

2.使用同步代码块
class Bank {
    private Bank() {
    }
    private static volatile Bank  bank = null;
    //为了避免出现指令重排,需要将bank声明为volatile
    public  static Bank getBank() {
        if(bank==null){
            synchronized (Bank.class){
                if (bank == null) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    bank = new Bank();
                }
            }
        }
        return bank;
    }
}
public class WindowTest {
    static Bank b1 = null;
    static Bank b2 = null;
    public static void main(String[] args) {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                b1 = Bank.getBank();
            }
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                b2 = Bank.getBank();
            }
        };
        t1.start();
        t2.start();
        try {
            t1.join();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        try {
            t2.join();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(b1);
        System.out.println(b2);
        System.out.println(b1==b2);
    }
}

死锁:

   线程的同步机制带来的问题:死锁
   如何看待死锁?
           不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步

           资源,就形成了线程的死锁。我们编写程序时,要避免出现死锁。

   举例:

public class WindowTest {
    public static void main(String[] args) {
        StringBuilder s1 = new StringBuilder();
        StringBuilder s2 = new StringBuilder();
        new Thread() {
            public void run() {
                synchronized (s1) {
                    s1.append("a");
                    s2.append("1");
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s2) {
                        s1.append("b");
                        s2.append("2");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();
        new Thread() {
            public void run() {
                synchronized (s2) {
                    s1.append("c");
                    s2.append("3");
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s1) {
                        s1.append("d");
                        s2.append("4");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();
    }
}

   诱发死锁的原因:

           互斥条件

           占用且等待

           不可抢夺(或不可抢占)

           循环等待

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

     解决死锁:

           死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。

           针对条件 1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。

           针对条件 2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。

           针对条件 3:占用部分资源的线程在进一步申请其他资源时,如果申请不到, 就主动

                                释放掉已经占用的资源。

           针对条件 4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免

                                循环等待问题。

(6)Lock锁

除了使用synchronized同步机制处理线程安全问题之外,还可以使用jdk5.0提供的Lock锁的方式 

步骤:
  1.创建Lock的实例,需要确保多个线程共用同一个Lock实例!需要考虑将此对象声明为
    static final
  2.执行Lock()方法,锁定对共享资源的调用
  3.unlock()的调用,释放对共享数据的锁定

例如:继承的方式
import java.util.concurrent.locks.ReentrantLock;
class SaleTicket extends Thread {
    static int ticket = 100;
    //创建Lock的实例,需要确保多个线程共用同一个Lock实例,需要考虑将此对象声明为static final
    private static final ReentrantLock look = new ReentrantLock();
    public void run() {
        while (true) {
            try {
                look.lock(); //执行lock方法,锁定对共享资源的调用
                if (ticket > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售票号" + ticket);
                    ticket--;
                } else {
                    break;
                }
            } finally {
                look.unlock(); //unlock()的调用,释放对共享数据的锁定
            }
        }
    }
}
public class WindowTest {
    public static void main(String[] args) {
        SaleTicket s1 = new SaleTicket();
        SaleTicket s2 = new SaleTicket();
        SaleTicket s3 = new SaleTicket();
        s1.start();
        s2.start();
        s3.start();
    }
}

synchronized同步的方式与Lock的对比?
     synchronized不管是同步代码块还是同步方法,都需要在结束一对{}之后,释放对同步监

     视器的调用。

     Lock是通过两个方法控制需要被同步的代码,更灵活一些。

     Lock作为接口,提供了多种实现类,适合更多更复杂的场景,效率更高。

(7) 线程间的通信机制

线程间通信的理解:
     当我们“需要多个线程、来共同完成一件任务,并且我们希望他们有规律的执行,那么多

     线程之间需要一些通信机制,可以协调它们的工作,以此实现多线程共同操作一份数据。

涉及到三个方法的使用:
     wait():线程一旦执行此方法,就进入等待状态。同时,会释放对同步监视器的调用

     notify():一旦执行此方法,就会唤醒被wait()的线程中优先级最高的那一个线程。

                      (如果被wait()的多个线程的优先级相同则随机唤醒一个)。被唤醒的线程

                        从当初被wait的位置继续执行。
     notifyAll():一旦执行此方法,就会唤醒酲所有被wait的线程。

例如:

使用两个线程打印 1-100。线程 1, 线程 2 交替打印:
class PrintNum  implements Runnable {
    int i = 1;
    public void run() {
        while (true) {
            synchronized (this) {
                notify();
                if (i <= 100) {
                    try{
                        Thread.sleep(100);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() +
                    ":" + i++);
                    try {
                        wait(); 
               //线程一旦执行此方法,就进入等待状态。同时,会释放对同步监视器的调用
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else{
                    break;
                }
            }
        }
    }
}
public class WindowTest {
    public static void main(String[] args) {
        PrintNum p=new PrintNum();
        Thread t1=new Thread(p,"线程1");
        Thread t2=new Thread(p,"线程2");
        t1.start();
        t2.start();
    }
}

注意点:
   此三个方法的使用,必须是在同步代码块或同步方法中。 
 (超纲:Lock需要配合Condition实现线程间的通信)

   此三个方法的调用者,必须是同步监视器。否则,会报IllegaLMonitorStateException异常

   此三个方法声明在0bject类中。

wait()和 sleep()的区别? 

    相同点:一旦执行,当前线程都会进入阻塞状态

    不同点:声明的位置:

                       wait():声明在0bject类中

                       sleep():声明在Thread类中,静态的
                  使用的场景不同:

                       wait():只能使用在同步代码块或同步方法中

                       sleep():可以在任何需要使用的场景

                  使用在同步代码块或同步方法中:

                       wait():一旦执行,会释放同步监视器

                       sleep():-旦执行,不会释放同步监视器
                  结束阻塞的方式:

                       wait():到达指定时间自动结束阻塞或通过被notify唤醒,结束阻塞

                       sleep():到达指定时间自动结束阻塞

(8) 创建线程的方式3,4:实现Callable和使用线程池

创建多线程的方式三:实现Callable(jdk5.0新增的)
    与Runnable方式的对比的好处:
         call()可以有返回值,更灵活
         call()可以使用throws的方式处理异常,更灵活
         Callable使用了泛型参数,可以指明具体的call()的返回值类型,更灵活

    缺点:

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

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class NumThread implements Callable {
    //2.实现 call 方法,将此线程需要执行的操作声明在 call()中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}
public class CallableTest {
    public static void main(String[] args) {
        //3.创建 Callable 接口实现类的对象
        NumThread numThread = new NumThread();
        //4.将此 Callable 接口实现类的对象作为传递到 FutureTask 构造器中,
        //创建 FutureTask 的对象
        FutureTask futureTask = new FutureTask(numThread);
        //5.将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Thread 对象,
        //并调用 start ()
        new Thread(futureTask).start();// 接收返回值
        try {
            //6.获取 Callable 中 call 方法的返回值
            //get()返回值即为 FutureTask 构造器参数 Callable 实现类重写的 call() 的返回值。
            Object sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

创建多线程的方式四:使用线程池

    现有问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束

    了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时

    间。 那么有没有一种办法使得线程可以复用,即执行完一个任务,并不被销毁,而是可以

    继续执行其他的任务?

    思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避

    免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

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

    举例:

import java.util.concurrent.*;
class NumberThread implements Runnable{
    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() +
                        ": " + i);
            }
        }
    }
}
class NumberThread1 implements Runnable{
    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName() +
                        ": " + i);
            }
        }
    }
}
class NumberThread2 implements Callable {
    @Override
    public Object call() throws Exception {
        int evenSum = 0;//记录偶数的和
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                evenSum += i;
            }
        }
        return evenSum;
    }
}
public class ThreadPoolTest {
    public static void main(String[] args) {
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        // 设置线程池的属性
        // System.out.println(service.getClass());//ThreadPoolExecutor
        service1.setMaximumPoolSize(50); //设置线程池中线程数的上限
        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于 Runnable
        service.execute(new NumberThread1());//适合适用于 Runnable
        try {
            Future future = service.submit(new NumberThread2());//适合使用于Callable
            System.out.println("总和为:" + future.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
        //3.关闭连接池
        service.shutdown();
    }
}

14. 常用类与基础API

(1)String的理解与不可变性

String类的理解(以JDK8为例说明):
   类的声明
      public final class String 

             implments java.io.Serializable,Comparable<String>,CharSequence
      final:String是不可被继承的
      Serializable:可序列化的接口。凡是实现此接口的类的对象就可以通过网络或本地流进

                             行数据的传输。

      Comparable:凡是实现此接口的类,其对象都可以比较大小。

      java.lang.String 类代表字符串。Java 程序中所有的字符串文字(例如 "hello" )都可以看

      作是实现此类的实例。

   内部声明的属性:

       jdk8中: private final char value[];存储字符串数据的容器
                      final:指明此value数组一旦初始化,其地址就不可变。

       jdk9开始:为了节省内存空间,做了优化
                         private final byte[] value;存储字符串数据的容器。

   字符串常量的存储位置:
       字符串常量都存储在字符串常量池(StringTable)中
       字符串常量池不允许存放两个相同的字符串常量。

       字符串常量池,在不同的jdk版本中,存放位置不同。

              jdk7之前。字符串常量池存放在方法区

              jdk7及之后:字符串常量池存放在堆空间。

   String a="222";
   String b="222";
   System.out.println(a==b); true

    String的不可变性:
       1️⃣当对字符串变量重新赋值时,会重新指定一个字符串常量的位置进行赋值,不会在

            原有的位置修改

       2️⃣当对现有的字符串进行拼接操作时,会重新开辟空间保存拼接以后的字符串,不会

            在原有的位置修改

       3️⃣当调用字符串的replace()替换现有的某个字符时,会重新开辟空间保存修改以后

            的字符串,不会在原有的位置修改

(2)String的实例化与连接操作 

String实例化的两种方式:
    第1种方式:String s1 ="hello";
    第2种方式:String s2 = new String("hello");

String s2 = new String("hello");在内存中创建了几个对象?

    两个!一个是堆空间中new的对象。另一个是在字符串常量池中生成的字面量。

例如:

String的连接操作:+

情况1:常量+常量:结果仍然存储在字符串常量池中。返回此字面量的地址
       注:此时的常量可能是字面量,也可能是final修饰的常量
情况2:常量+变量或变量+变量:都会通过new的方式创建一个新的字符串,返回堆空间中此字符串
       对象的地址
情况3:调用字符串的intern():返回的是字符串常量池中字面量的地址
情况4:concat(xxx):不管是常量调用此方法,还是变量调用,同样不管参数是常量还是变量,
      总之,调用完concat()方法都返回一个新new的对象。

String s1 = "hello";    
String s2 = "world";
String s3 = "helloworld";
String s4 = "hello" + "world";
String s5 = s1 + "world";
String s6 = "hello" + s2;
String s7 = s1 + s2;
String s8=s5.intern();
System.out.println(s3 == s4);  true
System.out.println(s3 == s5);  false
System.out.println(s3 == s6);  false
System.out.println(s3 == s7);  false
System.out.println(s5 == s6);  false
System.out.println(s5 == s7);  false
System.out.println(s3==s8);    true

final String s1 = "hello";
final String s2 = "world";
String s3 = "helloworld";
String s4 = "hello" + "world";
String s5 = s1 + "world";
String s6 = "hello" + s2;
String s7 = s1 + s2;
System.out.println(s3 == s4); true
System.out.println(s3 == s5); true
System.out.println(s3 == s6); true

String s1 = "hello";
String s2 = "world";
String s3 = s1.concat(s2);
String s4 = "hello".concat("world");
String s5 = s1.concat("world");
System.out.println(s3 == s4);  false
System.out.println(s3 == s5);  false
System.out.println(s4 == s5);  false

(3)Spring的构造器和常用方法:

构造器:
   public String() :初始化新创建的 String 对象,以使其表示空字符序列。
   String(String original): 初始化一个新创建的 String 对象,使其表示一个与参数相同的
                             字符序列;换句话说,新创建的字符串是该参数字符串的副本。
   public String(char[] value) :通过当前参数中的字符数组来构造新的String。
   public String(char[] value,int offset, int count) :通过字符数组的一部分来构造新
                                                       的 String。
   public String(byte[] bytes) :通过使用平台的默认字符集解码当前参数中的字节数组来构
                                 造新的 String。
   public String(byte[] bytes,String charsetName) :通过使用指定的字符集解码当前参数
                                                    中的字节数组来构造新的 String。


String 与其他结构间的转换:
  字符串 --> 基本数据类型、包装类: 
     Integer 包装类的 public static int parseInt(String s):可以将由“数字”字符组成的
                                                           字符串转换为整型。
     类似地,使用 java.lang 包中的 Byte、Short、Long、Float、Double 类调相应的类方法
     可以将由“数字”字符组成的字符串,转化为相应的基本数据类型。
  基本数据类型、包装类 --> 字符串:
     调用 String 类的 public String valueOf(int n)可将 int 型转换为字符串
     相应的 valueOf(byte b)、valueOf(long l)、valueOf(float f)、valueOf(double d)、
     valueOf(boolean b)可由参数的相应类型到字符串的转换。
  字符数组 --> 字符串:
     String 类的构造器:String(char[]) 和 String(char[],int offset,int length) 
     分别用字符数组中的全部字符和部分字符创建字符串对象。
  字符串 --> 字符数组:
     public char[] toCharArray():将字符串中的全部字符存放在一个字符数组中的方法。
     public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin):
     提供了将指定索引范围内的字符串存放到数组中的方法。
  字符串 --> 字节数组:(编码)
     public byte[] getBytes() :使用平台的默认字符集将此 String 编码为 byte 序列,并
                                将结果存储到一个新的 byte 数组中。
     public byte[] getBytes(String charsetName) :使用指定的字符集将此 String 编码
                                          到byte 序列,并将结果存储到新的 byte 数组
  字节数组 --> 字符串:(解码)
     String(byte[]):通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的String。
     String(byte[],int offset,int length):用指定的字节数组的一部分,即从数组起始位置
                                         offset开始取 length个字节构造一个字符串对象
     String(byte[], String charsetName ) 或 new String(byte[], int, int,String 
     charsetName ):解码,按照指定的编码方式进行解码。
常用方法:
   boolean isEmpty():字符串是否为空 
   int length():返回字符串的长度 
   String concat(xx):拼接 
   boolean equals(Object obj):比较字符串是否相等,区分大小写 
   boolean equalsIgnoreCase(Object obj):比较字符串是否相等,不区分大小写 
   int compareTo(String other):比较字符串大小,区分大小写,按照 Unicode 编码值比较大小 
   int compareToIgnoreCase(String other):比较字符串大小,不区分大小写 
   String toLowerCase():将字符串中大写字母转为小写 
   String toUpperCase():将字符串中小写字母转为大写 
   String trim():去掉字符串前后空白符 
   public String intern():结果在常量池中共享
查找:
   boolean contains(xx):是否包含 xx 
   int indexOf(xx):从前往后找当前字符串中xx,即如果有返回第一次出现的下标,要是没有返回-1 
   int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的
                                           索引,从指定的索引开始 
   int lastIndexOf(xx):从后往前找当前字符串中 xx,即如果有返回最后一次出现的下标,要是
                        没有返回-1 
   int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出
                                               现处的索引,从指定的索引开始反向搜索。
字符串截取:
   String substring(int beginIndex) :返回一个新的字符串,它是此字符串的从 beginIndex 
                                      开始截取到最后的一个子字符串。 
   String substring(int beginIndex, int endIndex):返回一个新字符串,它是此字符串
                                  beginIndex开始截取到endIndex(不包含)的一个子字符串
和字符/字符数组相关:
   char charAt(index):返回[index]位置的字符 
   char[] toCharArray(): 将此字符串转换为一个新的字符数组返回 
   static String valueOf(char[] data) :返回指定数组中表示该字符序列的 String 
   static String valueOf(char[] data, int offset, int count) :返回指定数组中表示该
                                                                 字符序列的 String 
   static String copyValueOf(char[] data): 返回指定数组中表示该字符序列的 String 
   static String copyValueOf(char[] data, int offset, int count):返回指定数组中表示
                                                               该字符序列的 String
开头与结尾 
   boolean startsWith(xx):测试此字符串是否以指定的前缀开始 
   boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子
                                                   字符串是否以指定前缀开始 
   boolean endsWith(xx):测试此字符串是否以指定的后缀结束
替换:
   String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用
                  newChar 替换此字符串中出现的所有 oldChar 得到的。 不支持正则
   String replace(CharSequence target, CharSequence replacement):使用指定的
                     字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串 
   String replaceAll(String regex, String replacement):使用给定的replacement 
                                  替换此字符串所有匹配给定的正则表达式的子字符串
   String replaceFirst(String regex, String replacement):使用给定的 replacement 
                                   替换此字符串匹配给定的正则表达式的第一个子字符串。

(4)StringBuilder、StringBuffer源码分析与常用方法

字符串相关类之可变字符序列:StringBuffer、StringBuilder

       ​​​​​​​因为 String 对象是不可变对象,虽然可以共享常量对象,但是对于频繁字符串 的修改和

       拼接操作,效率极低,空间消耗也比较高。因此,JDK 又在 java.lang 包提供了可变字

       符序列 StringBuffer 和 StringBuilder 类型。

三个类的对比:String、StringBuffer、StringBuilder

       String:不可变的字符序列;底层使用char[](jdk8及之前),底层使用byte[](jdk9及之

                    后)

       StringBuffer:可变的字符序列;JDK1.0声明,线程安全的,效率低;底层使用char[]

                            (jdk8及之前),底层使用byte[](jdk9及之后)

       StringBuilder:可变的字符序列;JDK5.0声明,线程不安全的,效率高;底层使用char[]

                               (jdk8及之前),底层使用byte[](jdk9及之后)

源码启示:
   如果开发中需要频繁的针对于字符串进行增、删、改等操作,建议使用StringBuffer或

   StringBuilder替换String.因为使用String效率低。
   如果开发中,不涉及到线程安全问题,建议使用StringBuilder替换StringBuffer。因为使用

   StringBuilder效率高

   如果开发中大体确定要操作的字符的个数,建议使用带int capacity参数的构造器。因为可

   以避免底层多次扩容操作,性能更高  

对比三者的执行效率:
   StringBuilder > StringBuffer > String

常用方法:

StringBuilder、StringBuffer 的 API 是完全一致的,并且很多方法与 String 相同。

常用 API:
StringBuffer append(xx):提供了很多的 append()方法,用于进行字符串追加的方式拼接
StringBuffer delete(int start, int end):删除[start,end)之间字符 
StringBuffer deleteCharAt(int index):删除[index]位置字符 
StringBuffer replace(int start,int end,String str):替换[start,end)范围字符序列为str 
void setCharAt(int index, char c):替换[index]位置字符 
char charAt(int index):查找指定 index 位置上的字符 
StringBuffer insert(int index, xx):在[index]位置插入 xx 
int length():返回存储的字符数据的长度 
StringBuffer reverse():反转,当append和insert时,如果原来value数组长度不够,可扩容
如上(1)(2)(3)(4)(9)这些方法支持方法链操作。

其它 API:
int indexOf(String str):在当前字符序列中查询 str 的第一次出现下标
int indexOf(String str, int fromIndex):在当前字符序列[fromIndex,最后]中查询 str 
                                        的第一次出现下标 
int lastIndexOf(String str):在当前字符序列中查询 str 的最后一次出现下标 
int lastIndexOf(String str, int fromIndex):在当前字符序列[fromIndex,最后]中查询 
                                            str 的最后一次出现下标 
String substring(int start):截取当前字符序列[start,最后] 
String substring(int start, int end):截取当前字符序列[start,end) 
String toString():返回此序列中数据的字符串表示形式 
void setLength(int newLength) :设置当前字符序列长度为 newLength

例如:String str="1";
     StringBuffer sb=new StringBuffer("2");
     sb.append(str);
     System.out.println(sb);  //21

(5)JDK8之前日期时间API的使用: Date、 SimpleDateFormat,、 Calendar 

System类的currentTimeMillis()
   获取当前时间对应的毫秒数,long类型,时间戳
   当前时间与1978年1月1日0时盼瞅之间的毫秒数
   常用来计算时间差
   例如:long time = System.currentTimeMillis();
        System.out.println(time);

Date类的使用:
  1️⃣java.util.Date 表示特定的瞬间,精确到毫秒。
     构造器:
       Date():使用无参构造器创建的对象可以获取本地当前时间。
       Date(long 毫秒数):把该毫秒值换算成日期时间对象
     常用方法:
       getTime(): 返回自1970年1月1日00:00:00 GMT以来此Date对象表示的毫秒数。
       toString(): 把此 Date 对象转换为以下形式的 String: dow mon dd hh:mm:ss zzz yyyy 
                   其中: dow 是一周中的某一天 (Sun, Mon, Tue, Wed, Thu, Fri, Sat),zzz 
                   是时间标准。
     其它很多方法都过时了。
     例如:
       Date date = new Date();//创建一个基于当前系统时间的Date的实例
       System.out.println(date.toString()); //Sat Jul 13 09:05:13 CST 2024
       System.out.println("对应的毫秒数:" + date.getTime());//对应的毫秒数:1720832713571
       Date date2 = new Date(12123232123233L); //创建一个基于指定时间戳的Date的实例
       System.out.println(date2.toString()); //Thu Mar 04 12:28:43 CST 2354
  2️⃣java.sql.Date 对应着数据库中的date类型
    例如:
        java.sql.Date date3 = new java.sql.Date(12123232123233L);
        System.out.println(date3.toString()); 
        System.out.println(date3.getTime());

java.text.SimpleDateFormat:
  java.text.SimpleDateFormat 类是一个不与语言环境有关的方式来格式化和解析日期的具体类。
  可以进行格式化:日期 --> 文本
  可以进行解析:文本 --> 日期
  构造器: 
    SimpleDateFormat() :默认的模式和语言环境创建对象
    public SimpleDateFormat(String pattern):该构造方法可以用参数 pattern指定的格式创
                                             建一个对象
  格式化:
    public String format(Date date):方法格式化时间对象 date
  解析:
    public Date parse(String source):从给定字符串的开始解析文本,以生成一个日期。
  例如:
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    Date date = new Date();
    String strDate = sdf.format(date);
    System.out.println(strDate); //2024-07-13 09:18:16
    Date date2= sdf.parse("2024-07-13 09:18:16");
    System.out.println(date2); //Sat Jul 13 09:18:16 CST 2024

java.util.Calendar(日历):
  Date 类的 API 大部分被废弃了,替换为 Calendar。
  Calendar 类是一个抽象类,主用用于完成日期字段之间相互操作的功能。
  获取 Calendar 实例的方法:
    使用 Calendar.getInstance()方法
    调用它的子类 GregorianCalendar(公历)的构造器。
  一个 Calendar 的实例是系统时间的抽象表示,可以修改或获取 YEAR、MONTH、DAYOFWEEK、
  HOUROFDAY 、MINUTE、SECOND 等 日历字段对应的时间值。
    public int get(int field):返回给定日历字段的值
    public void set(int field,int value) :将给定的日历字段设置为指定的值
    public void add(int field,int amount):根据日历的规则,为给定的日历字段添加或
                                           者减去指定的时间量
    public final Date getTime():将 Calendar 转成 Date 对象
    public final void setTime(Date date):使用指定的 Date 对象重置 Calendar的时间
  常用字段:
    YEAR           年
    MONTH          月(从0开始,可以+1使用)
    DAY_OF_MONTH   月中的天(几号)
    HOUR           时(12小时制)
    HOUR_OF_DAY    时(24小时制)
    MINUTE         分
    SECOND         秒
    DAY_OF_WEEX    周中的天(周几,周日为1,可以-1使用)
  例如:
    Calendar calendar=Calendar.getInstance();
    System.out.println(calendar.get(Calendar.DAY_OF_MONTH)); //13
    System.out.println(calendar.get(Calendar.DAY_OF_YEAR));  //195
    calendar.set(Calendar.DAY_OF_MONTH,23);
    System.out.println(calendar.get(Calendar.DAY_OF_MONTH)); //23

将一个java.util.Date的实例转换为java.sql.Date的实例
    Date date = new Date();
    java.sql.Date date1 = new java.sql.Date(date.getTime());
    System.out.println(date1);

(6)JDK8中新的日期时间API的使用

JDK 1.0 中包含了一个 java.util.Date 类,但是它的大多数方法已经在 JDK 1.1 引入 Calendar 类之后被弃用了。而 Calendar 并不比 Date 好多少。它们面临的问题是:

      可变性:像日期和时间这样的类应该是不可变的。

      偏移性:Date 中的年份是从 1900 开始的,而月份都从 0 开始。

      格式化:格式化只对 Date 有用,Calendar 则不行。

      此外,它们也不是线程安全的;不能处理闰秒等。

Java 8 以一个新的开始为 Java 创建优秀的 API。新的日期时间 API 包含:

      java.time – 包含值对象的基础包

      java.time.chrono – 提供对不同的日历系统的访问。

      java.time.format – 格式化和解析时间和日期

      java.time.temporal – 包括底层框架和扩展特性

      java.time.zone – 包含时区支持的类

      说明:新的 java.time 中包含了所有关于时钟(Clock),本地日期 (LocalDate)、本地

                 时间(LocalTime)、本地日期时间(LocalDateTime)、 时区 

               (ZonedDateTime)和持续时间(Duration)的类。

      尽管有 68 个新的公开类型,但是大多数开发者只会用到基础包和 format 包, 大概占总

      数的三分之一


本地日期时间:LocalDate、LocalTime、LocalDateTime​​​​​​​

方法描述
now()/ now(ZoneId zone)静态方法,根据当前时间创建对象/指定时区的对象
of(xx,xx,xx,xx,xx,xxx)静态方法,根据指定日期/时间创建对象
getDayOfMonth()/getDayOfYear()获得月份天数(1-31) /获得年份天数(1-366)
getDayOfWeek()获得星期几(返回一个 DayOfWeek 枚举值)
getMonth()获得月份, 返回一个 Month 枚举值
getMonthValue() / getYear()获得月份(1-12) /获得年份
getHours()/getMinute()/getSecond()获得当前对象对应的小时、分钟、秒
withDayOfMonth()/withDayOfYear()/withMonth()/withYear()将月份天数、年份天数、月份、年份修改为指定的值并返回新的对象
with(TemporalAdjuster t)将当前日期时间设置为校对器指定的日期时间
plusDays(), plusWeeks(), plusMonths(), plusYears(),plusHours()向当前对象添加几天、几周、几个月、几年、几小时
minusMonths() / minusWeeks()/minusDays()/minusYears()/minusHours()从当前对象减去几月、几周、几天、几年、几小时
plus(TemporalAmount t)/minus(TemporalAmount t)添加或减少一个 Duration 或 Period
isBefore()/isAfter()比较两个 LocalDate
isLeapYear()判断是否是闰年(在LocalDate类中声明)
format(DateTimeFormatter t)格式化本地日期、时间,返回一个字符串
parse(Charsequence text)将指定格式的字符串解析为日期、时间

例如:

LocalDate localDate = LocalDate.now();
LocalTime localTime = LocalTime.now();
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDate);  //2024-07-13
System.out.println(localTime);  //10:57:14.550889300
System.out.println(localDateTime); //2024-07-13T10:57:14.550889300
//of():获取指定的日期、时间对应的实例
LocalDate localDate1 = LocalDate.of(2021, 5, 23);
LocalDateTime localDateTime1 = LocalDateTime.of(2022, 12, 5, 11, 23, 45);
System.out.println(localDate1); //2021-05-23
System.out.println(localDateTime1); //2022-12-05T11:23:45

顺时:Instant:

     Instant:时间线上的一个瞬时点。 这可能被用来记录应用程序中的事件时间戳。

     时间戳是指格林威治时间 1970 年 01 月 01 日 00 时 00 分 00 秒(北京时间 1970 年 01 月

     01 日 08 时 00 分 00 秒)起至现在的总秒数。

 java.time.Instant表示时间线上的一点,而不需要任何上下文信息,例如,时区。概念

    上讲,它只是简单的表示自1970年1月1日0时0分0秒(UTC)开始的秒数。

方法描述
now()静态方法,返回默认UTC时区的Instant类的对象
ofEpochMilli(long epochMilli)静态方法,返回在1970-01-01 00:00:00基础上加上指定毫秒数之后的Instant类的对象
atOffset(ZoneOffset offset)结合即时的偏移来创建一个 OffsetDateTime
toEpochMilli()返回1970-01-01 00:00:00到当前时间的毫秒数,即为时间戳

例如;

 //now()获取当前时间
Instant instant = Instant.now();
System.out.println(instant);  //2024-07-13T03:14:04.610177200Z
Instant instant2 = Instant.ofEpochMilli(232142343435235l);
System.out.println(instant2); //9326-04-19T16:17:15.235Z
//toEpochMilli()获取当前毫秒数
long minllTime = instant.toEpochMilli(); 
System.out.println(minllTime); //1720840635062

日期时间格式化:DateTimeFormatter:

    该类提供了三种格式化方法:

       1️⃣(了解)预定义的标准格式。如:ISO_LOCAL_DATE_TIME、ISO_LOCAL_DATE、

                                                           ISO_LOCAL_TIME

       2️⃣(了解)本地化相关的格式。如:ofLocalizedDate(FormatStyle.LONG)

// 本地化相关的格式。如:ofLocalizedDateTime()
// FormatStyle.MEDIUM / FormatStyle.SHORT :适用于LocalDateTime			
// 本地化相关的格式。如:ofLocalizedDate()
// FormatStyle.FULL / FormatStyle.LONG / FormatStyle.MEDIUM / FormatStyle.SHORT : 适用于LocalDate

       3️⃣自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”)

ofPattern(String pattern)静态方法,返回一个指定字符串格式的DateTimeFormatter
format(TemporalAccessor t)格式化一个日期、时间,返回字符串
parse(CharSequence text)将指定格式的字符序列解析为一个日期、时间
例如: 
//自定义的格式: ofPattern("yyyy-MM-dd hh: mm: ss")
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
//格式化:日期、时间-->字符申
LocalDateTime localDateTime2 = LocalDateTime.now();
String strDateTime = dateTimeFormatter.format(localDateTime2);
System.out.println(strDateTime);//2024-07-13 11:27:13
//解析:字符申--->日期、时间
TemporalAccessor temporalAccessor = dateTimeFormatter.parse("2021-05-23 11:23:45");
LocalDateTime localDateTime3 = LocalDateTime.from(temporalAccessor);
System.out.println(localDateTime3); //2021-05-23T11:23:45

(7) 使用Comparable接口实现自然排序

在 Java 中经常会涉及到对象数组的排序问题,那么就涉及到对象之间的比较问题。

Java 实现对象排序的方式有两种: 自然排序:java.lang.Comparable

                                                         定制排序:java.util.Comparato

方式一:实现Comparable接口的方式
实现步骤:①具体的类A实现Comparable接口
                  ② 重写Comparable接口中的compareTo(0bject obj)方法,在此方法中指明比

                      较类A的对象的大小的标准

                  ③创建类A的多个实例,进行大小的比较或排序。

例如:

public class Product implements Comparable {
    private  String name;
    private  double price;
    public Product(String name,double price){
        this.name=name;
        this.price=price;
    }
    public  Product(){
    }
    public String getName(){
        return  name;
    }
    public  void  setName(String name){
       this.name=name;
    }
    public double getPrice(){
        return  price;
    }
    public void setPrice(double price){
        this.price=price;
    }
    public  String toString(){
        return name+price;
    }
    @Override  //在此方法中,指明如何判断当前类的对象的大小。
    public int compareTo(Object o) {
        if(o==this){
            return 0;
        }
       if(o instanceof  Product){
           Product p=(Product) o;
           return  Double.compare(this.price,p.price);
       }
       throw  new RuntimeException("类型不匹配");
    }
}

import java.util.Arrays;
public class CallableTest {
    public static void main(String[] args) {
        Product[] arr = new Product[3];
        arr[0] = new Product("小米", 1000);
        arr[1] = new Product("华为", 1500);
        arr[2] = new Product("vivo ", 800);
        Arrays.sort(arr);
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
}

方式二:实现Comparator接口的方式

实现步骤:

    ①创建一个实现了Coparator接口的实现类A 
    ②实现类A要求重写Comparator接口中的抽象方法compare(0bject o1,0bject o2),在

        此方法中指明要比较大小的对象的大小关系。(比如,String类、Product类)

    ③创建此实现类A的对象,并将此对象传入到相关方法的参数位置即可。(比如:

        Arrays.sort(..,类A的实例))

public class Product{
    private  String name;
    private  double price;
    public Product(String name,double price){
        this.name=name;
        this.price=price;
    }
    public  Product(){
    }
    public String getName(){
        return  name;
    }
    public  void  setName(String name){
    this.name=name;
    }
    public double getPrice(){
        return  price;
    }
    public void setPrice(double price){
        this.price=price;
    }
    public  String toString(){
        return name+price;
    }
}

import java.util.Arrays;
import java.util.Comparator;
public class CallableTest {
    public static void main(String[] args) {
        Product[] arr = new Product[3];
        arr[0] = new Product("小米", 1000);
        arr[1] = new Product("华为", 1500);
        arr[2] = new Product("vivo ", 800);
        //创建一个实现了Comparator接口的实现类的对象
        Comparator comparator=new Comparator() {
            //如果判断两个对象o1,o2的大小,其标准就是此方法的方法体要编写的逻辑。
            @Override
            public int compare(Object o1, Object o2) {
                if(o1 instanceof Product&&o2 instanceof Product){
                    Product p1 = (Product) o1;
                    Product p2 = (Product) o2;
                    return Double.compare(p1.getPrice(), p2.getPrice());
                }
                throw new RuntimeException("类型不匹配");
            }
        };
        Arrays.sort(arr,comparator);
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
}

对比两种方式:

    角度一:
        自然排序:单一的,唯一的

        定制排序:灵活的,多样的
    角度二:
        自然排序:一劳永逸的
        定制排序:临时的
    角度三:细节
        自然排序:对应的接口是Comparable,对应的抽象方法compareTo(0bject obj)

        定制排序:对应的接口是Comparator,对应的抽象方法compare(0bject obj1,0bject

                          obj2)

(8)其他常用类的使用 

java.lang.System 类
  System 类代表系统,系统级的很多属性和控制方法都放置在该类的内部。该类位于java.lang 包。
  由于该类的构造器是 private 的,所以无法创建该类的对象。其内部的成员变量和成员方法都是
  static 的,所以也可以很方便的进行调用。
  成员变量 Scanner scan = new Scanner(System.in);
      System 类内部包含 in、out 和 err 三个成员变量,分别代表标准输入流(键盘输入),标
      准输出流(显示器)和标准错误输出流(显示器)。
  成员方法
      1️⃣native long currentTimeMillis(): 该方法的作用是返回当前的计算机时间,时间的表
      达格式为当前计算机时间和 GMT 时间(格林威治时间)1970 年 1 月 1 号 0 时 0 分 0 秒
      所差的毫秒数。
      2️⃣void exit(int status): 该方法的作用是退出程序。其中 status 的值为 0 代表正常
        退出,非零代表异常退出。使用该方法可以在图形界面编程中实现程序的退出功能等。
      3️⃣ void gc(): 该方法的作用是请求系统进行垃圾回收。至于系统是否立刻回收,则取决于
         系统中垃圾回收算法的实现以及系统执行时的情况。
      4️⃣ String getProperty(String key): 该方法的作用是获得系统中属性名为 key 的属性
          对应的值。系统中常见的属性名以及属性的作用如下表
               属性名               属性说明
            java.version        Java运行时环境版本
            java.home           Java安装目录
            os.nanme            操作系统的名称
            os.version          操作系统的版本
            user.name           用户的账户名称
            user.home           用户的主目录
            user.dir            用户的当前工作目录

java.lang.Runtime 类:
   每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。
   public static Runtime getRuntime(): 返回与当前 Java 应用程序相关的运行时对象。
                                        应用程序不能创建自己的 Runtime 类实例。
   public long totalMemory():返回 Java 虚拟机中初始化时的内存总量。此方法返回的
             值可能随时间的推移而变化,这取决于主机环境。默认为物理电脑内存的 1/64。
   public long maxMemory():返回 Java 虚拟机中最大程度能使用的内存总量。默认为物
                            理电脑内存的 1/4。
   public long freeMemory():返回 Java 虚拟机中的空闲内存量。调用 gc 方法可能导致
                            freeMemory 返回值的增加。

java.lang.Math
   java.lang.Math 类包含用于执行基本数学运算的方法,如初等指数、对数、平方根和三角函数。
                  类似这样的工具类,其所有方法均为静态方法,并且不会创建对象,调用起来
                  非常简单。
   public static double abs(double a) :返回 double 值的绝对值。
   public static double ceil(double a) :返回大于等于参数的最小的整数。
   public static double floor(double a) :返回小于等于参数最大的整数。
   public static long round(double a) :返回最接近参数的 long。(相当于四舍五入方法)
   public static double pow(double a,double b):返回 a 的 b 幂次方法
   public static double sqrt(double a):返回 a 的平方根
   public static double random():返回[0,1)的随机值
   public static final double PI:返回圆周率
   public static double max(double x, double y):返回 x,y 中的最大值
   public static double min(double x, double y):返回 x,y 中的最小值
   其它:acos,asin,atan,cos,sin,tan 三角函数

BigInteger:
   Integer 类作为 int 的包装类,能存储的最大整型值为 2^31-1,Long 类也是有限的,
   最大为 2^63-1。如果要表示再大的整数,不管是基本数据类型还是他们的包装类都无能为力,
   更不用说进行运算了。
   java.math 包的 BigInteger 可以表示不可变的任意精度的整数。BigInteger 提供所有
   Java 的基本整数操作符的对应物,并提供 java.lang.Math 的所有相关方法。另外,BigInteger
   还提供以下运算:模算术、GCD 计算、质数测试、素数生成、位操作以及一些其他操作。
   构造器:
     BigInteger(String val):根据字符串构建 BigInteger 对象
   方法:
     public BigInteger abs():返回此 BigInteger 的绝对值的 BigInteger
     BigInteger add(BigInteger val) :返回其值为 (this + val) 的 BigInteger
     BigInteger subtract(BigInteger val) :返回其值为 (this - val) 的BigInteger
     BigInteger multiply(BigInteger val) :返回其值为 (this * val) 的BigInteger
     BigInteger divide(BigInteger val) :返回其值为 (this / val) 的BigInteger。
                                         整数相除只保留整数部分。
     BigInteger remainder(BigInteger val) :返回其值为 (this % val) 的BigInteger。
     BigInteger[] divideAndRemainder(BigInteger val):返回包含 (this / val) 后跟
                                           (this % val) 的两个 BigInteger 的数组。
     BigInteger pow(int exponent) :返回其值为 (this^exponent) 的BigInteger。

BigDecimal:
   一般的 Float 类和 Double 类可以用来做科学计算或工程计算,但在商业计算中,要求数字精
   度比较高,故用到 java.math.BigDecimal 类。
  BigDecimal 类支持不可变的、任意精度的有符号十进制定点数。
  构造器: 
     public BigDecimal(double val)
     public BigDecimal(String val) --> 推荐
  常用方法
     public BigDecimal add(BigDecimal augend)
     public BigDecimal subtract(BigDecimal subtrahend)
     public BigDecimal multiply(BigDecimal multiplicand)
     public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode):
     divisor 是除数,scale 指明保留几位小数,roundingMode指明舍入模式(ROUNDUP :向上加1、
     ROUNDDOWN :直接舍去、ROUNDHALFUP:四舍五入)

java.util.Random:
   用于产生随机数:
      boolean nextBoolean():返回下一个伪随机数,它是取自此随机数生成器序列的均匀分布的 
                            boolean 值。
      void nextBytes(byte[] bytes):生成随机字节并将其置于用户提供的 byte 数组中。
      double nextDouble():返回下一个伪随机数,它是取自此随机数生成器序列的、在0.0 和 1.0
                          之间均匀分布的 double 值。
      float nextFloat():返回下一个伪随机数,它是取自此随机数生成器序列的、在0.0 和 1.0 之
                        间均匀分布的 float 值。
      double nextGaussian():返回下一个伪随机数,它是取自此随机数生成器序列的、呈高斯(“正态”)
                            分布的 double 值,其平均值是 0.0,标准差是 1.0。
      int nextInt():返回下一个伪随机数,它是此随机数生成器的序列中均匀分布的int 值。
      int nextInt(int n):返回一个伪随机数,它是取自此随机数生成器序列的、在 0(包括)和指定值
                        (不包括)之间均匀分布的 int 值。
      long nextLong():返回下一个伪随机数,它是取自此随机数生成器序列的均匀分布的 long 值。

15. 集合框架 

 (1)数组的特点、弊端与集合框架体系介绍

数组存储多个数据方面的特点:  

     数组一旦初始化,其长度就是确定的。

     数组中的多个元素是依次紧密排列的,有序的,可重复的
  (优点)数组一旦初始化完成,其元素的类型就是确定的。不是此类型的元素,就不能添加

    到此数组中。

  (优点)元素的类型既可以是基本数据类型,也可以是引用数据类型

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

Java 集合框架中的类可以用于存储多个对象,还可用于保存具有映射关系的关联数组。

Java 集合可分为 Collection 和 Map 两大体系:

Collection接口:用于存储一个一个的数据,也称单列数据集合

      List子接口:用来存储有序的、可以重复的数据(主要用来替换数组,"动态"数组)

           实现类:ArrayList(主要实现类)、LinkedList、Vector

      Set子接口:用来存储无序的、不可重复的数据(类似于高中讲的"集合")

           实现类:HashSet(主要实现类)、LinkedHashSet、TreeSet

Map接口:用于存储具有映射关系“key-value对”的集合,即一对一对的数据,也称双列数据集合。(类似于高中的函数、映射。(x1,y1),(x2,y2) ---> y = f(x) )

       HashMap(主要实现类)、LinkedHashMap、TreeMap、Hashtable、Properties

集合框架全图:

Collection接口继承:

Map接口继承树:

(2)Collection接口中的方法使用

JDK不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set和List)去实现。
Collection 接口是 List和Set接口的父接口,该接口里定义的方法既可用于操作 Set 集合,
也可用于操作 List 集合。方法如下:

添加:
  add(E obj):添加元素对象到当前集合中
  addAll(Collection other):添加other集合中的所有元素对象到当前集合中,
                            即this=this∪other
判断:
  int size():获取当前集合中实际存储的元素个数
  boolean isEmpty():判断当前集合是否为空集合
  boolean contains(Object obj):判断当前集合中是否存在一个与obj对象equals返回true
                                的元素
  boolean containsAll(Collection coll):判断coll集合中的元素是否在当前集合中都存在,
                                       即coll集合
  boolean equals(Object obj):判断当前集合与obj是否相等
删除:
  void clear():清空集合元素
  boolean remove(Object obj) :从当前集合中删除第一个找到的与obj对象equals返回true
                               的元素。
  boolean removeAll(Collection coll):从当前集合中删除所有与coll集合中相同的元素。
                                      即this = this - this ∩ coll
  boolean retainAll(Collection coll):从当前集合中删除两个集合中不同的元素,使得
                                      当前集合仅保留与coll集合中的元素相同的元素,
                                      即当前集合中仅保留两个集合的交集,即this =
                                      this ∩ coll
其它:
  Object[] toArray():返回包含当前集合中所有元素的数组
  hashCode():获取集合对象的哈希值
  iterator():返回迭代器对象,用于集合遍历

例如:
  public  void test(){
        Collection coll=new ArrayList();
        coll.add("aa");
        coll.add(123);//自动装箱
        coll.add("尚硅谷");
        coll.add(new Object());
        System.out.println(coll); //[aa, 123, 尚硅谷, java.lang.Object@4e41089d]
        System.out.println(coll.size());//4
        System.out.println(coll.isEmpty());//false
        System.out.println(coll.contains("11"));//false
        System.out.println(coll.contains(123));//true

        Collection coll1=new ArrayList();
        coll1.add("BB");
        coll1.add(456);
        System.out.println(coll.containsAll(coll1));//false
        coll.addAll(coll1);
        System.out.println(coll);//[aa, 123, 尚硅谷, java.lang.Object@4e41089d, BB, 456]
        System.out.println(coll.size());//6
    }

注意:coll.addAll(other);与coll.add(other);

集合与数组的相互转换:
   集合--->数组:toArray()
   数组--->集合:调用Arrays的静态方法asList(0bject...objs)

向Collection中添加元素的要求:

   要求元素所属的类一定要重写equals()!

   原因:因为Collection中的相关方法(比如:contains()/remove())在使用时,要调用

              元素所在类的equals()。


Iterator接口:

    在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK专门提供了一个接

    口java.util.IteratorIterator接口也是Java集合中的一员,但它与CollectionMap

 接口有所不同

         Collection接口与Map接口主要用于存储元素

   Iterator,被称为迭代器接口,本身并不提供存储对象的能力,主要用于遍历

   Collection中的元素

    Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现

    了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对

    象。

   public Iterator iterator(): 获取集合对应的迭代器,用来遍历集合中的元素的。

         集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第

         一个元素之前

    Iterator接口的常用方法如下:

   public E next():返回迭代的下一个元素。

   public boolean hasNext():如果仍有元素可以迭代,则返回 true

    注意:在调用it.next()方法之前必须要调用it.hasNext()进行检测。若不调用,且下一条记录

               无效,直接调用it.next()会抛出NoSuchElementException异常

​​​​​​迭代器(Iterator)的作用:用来追历集合元素的。​

如何获取迭代器(Iterator)对象并实现遍历(代码实现):

Collection coll=new ArrayList();
coll.add("aa");
coll.add(123);//自动装箱
coll.add("尚硅谷");
coll.add(new Object());
Iterator itr=coll.iterator(); //获取迭代器对象
for (int i=0;i<coll.size();i++){
     System.out.println(itr.next());  //实现遍历方法1
}
while(itr.hasNext()){
     System.out.println(itr.next()); //实现遍历方法2
}

迭代器的执行原理:

Iterator迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素,接下来通过一个图例来演示Iterator对象迭代元素的过程:

增强for循环(foreach循环)的使用(jdk5.新特性):

作用:用来遍历数组、集合。
格式:
  for(要遍历的集合或数组元素的类型  临时变量:要遍历的集合或数组变量){
            操作临时变量的输出
  }
  例如:
       Collection coll=new ArrayList();
       coll.add("aa");
       coll.add(123);//自动装箱
       coll.add("尚硅谷");
       coll.add(new Object());
       for (Object obj:coll){
           System.out.println(obj);
       }
说明:
  针对于集合来讲,增强for循环的底层仍然使用的是迭代器。增强for循环的执行过程中,
  是将集合或数组中的元素依次赋值给临时变量,注意,循环体中对临时变量的修改,
  可能不会导致原有集合或数组中元素的修改。

(3)List接口的常用方法:

鉴于Java中数组用来存储数据的局限性,我们通常使用java.util.List替代数组

List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。

JDK API中List接口的实现类常用的有:ArrayListLinkedListVector

List接口方法:

List除了从Collection集合继承的方法外,List集合里添加了一些`根据索引`来操作集合元素的方法

插入元素
  void add(int index, Object ele)`:在index位置插入ele元素
  boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
获取元素
  Object get(int index)`:获取指定index位置的元素
  List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合
获取元素索引
  int indexOf(Object obj):返回obj在集合中首次出现的位置
  int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
删除和替换元素
  Object remove(int index)`:移除指定index位置的元素,并返回此元素
  Object set(int index, Object ele)`:设置指定index位置的元素为ele

例如:
  List list = new ArrayList();
  list.add("111");
  list.add("222");
  list.add(1,"333");
  System.out.println(list); //[111, 333, 222]

注意:
  list.remove(2); 删除的是索引为2的元素
  list.remove(Integer.valueOf(2)); 删除的是数据2

ArrayList: List的主要实现类;线程不安全的、效率高;底层使用0bject[]数组存储

                   在添加数据、查找数据时,效率较高;在插入、删除数据时,效率较低

LinkedList:底层使用双向链表的方式进行存储;在对集合中的数据进行频繁的删除、插入

                     操作时,建议使用。在插入、删除数据时,效率较高;在添加数据、查找数据

                     时,效率较 低;

                     特有方法:void addFirst(Object obj)

                                       void addLast(Object obj)

                                       Object getFirst()

                                       Object getLast()

                                       Object removeFirst()

                                       Object removeLast()

Vector:List的古老实现类: 线程安全的、效率低;底层使用0bject[]数组存储

                                           特有方法:void addElement(Object obj)

                                                             void insertElementAt(Object obj,int index)

                                                             void setElementAt(Object obj,int index)

                                                             void removeElement(Object obj)

                                                             void removeAllElements()

(4)Set不同实现类的对比及Set无序性、不可重复性的剖析 :

​​​​​​​Set及其实现类特点:

java.util.Collection:存储一个一个的数据
|----子接口:set:存储无序的、不可重复的数据(高中学习的集合)

      l----HashSet:主要实现类;底层使用的是HashMap,即使用数组+单向链表+红黑树结构

                             进行存储。(jdk8中)

            l----LinkedHashSet:是HashSet的子类;在现有的数组+单向链表+红黑树结构的基

                                             础上,又添加了一组双向链表,用于记录添加元素的先后顺序。

                                             即:我们可以按照添加元素的顺序实现遍历。便于频繁的查询操

                                             作。
      |----TreeSet:底层使用红黑树存储。可以按照添加的元素的指定的属性的大小顺序进行

                            遍历。

 开发中的使用频率及场景:

     较List,Map来说,Set使用的频率比较少。

     用来过滤重复数据

   Set set = new HashSet();
   set.add("aa");
   set.add(123);
   set.add(new Object());
   Iterator integer=set.iterator();
   while (integer.hasNext()){
       System.out.println(integer.next());
   }

Set中常用方法:即为Collection中声明的15个抽象方法。没有新增的方法。

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

无序性:!=随机性。
              添加元素的顺序和遍历元素的顺序不一致,是不是就是无序性呢?No!到底什么是

              无序性?与添加的元素的位置有关,不像ArrayList一样是依次紧密排列的。这里是

              根据添加的元素的哈希值,计算的其在数组中的存储位置。此位置不是依次排列

              的,表现为无序性。

不可重复性:添加到Set中的元素是不能相同的。
              比较的标准,需要判断hashCode()得到的哈希值以及equals()得到的boolean

              型的结果。哈希值相同且equals()返回true,则认为元素是相同的

添加到HashSet  /LinkedHashSet中元素的要求:
    要求元素所在的类要重写两个方法:equals()和 hashCode()。
    同时,要求equals()和hashCode()要保持一致性!我们只需要在IDEA中自动生成两

    个方法的重写即可,即能保证两个方法的一致性。

TreeSet的使用:
    底层的数据结构:红黑树
    添加数据后的特点:可以按照添加的元素的指定的属性的大小顺序进行遍历。

    向TreleSet中添加的元素的要求:
         要求添加到TreeSet中的元素必须是同一个类型的对象,否则会报ClassCastException.

         添加的元素需要考虑排序:①自然排序②定制排序

    判断数据是否相同的标准:
         1️⃣不再是考虑hashCode()和equals()方法了,也就意味着添加到TreeSet中的元

              素所在的类不需要重写hashCode()和equals()方法了
         2️⃣比较元素大小的或比较元素是否相等的标准就是考虑自然排序或定制排序中,

               compareTo()或compare()的返回值。如果compareTo()或compare()的返回值

               为0,则认为两个对象是相等的。由于TreeSet中不能存放相同的元素,则后一个相

              等的元素就不能添加到TreeSet中。

TreeSet set=new TreeSet();
set.add("CC");
set.add("AA");
set.add("BB");
set.add("MM");
set.add("SS");
set.add(123);
Iterator iterator=set.iterator();
while (iterator.hasNext()){
     System.out.println(iterator.next());
}

自然排序:
public class Student implements Comparable {
    private int age;
    private String name;
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    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 int compareTo(Object o) {
        if (this == o) { 
            return 0;
        }
        if (o instanceof Student) {
            Student s = (Student) o;
            return this.age - s.age;
        }
        throw new RuntimeException("类型不匹配");
    }
}
public class MyTreeSet2 {
    public static void main(String[] args) {
        TreeSet ts = new TreeSet();     //创建集合对象
        Student s1 = new Student("zhangsan",28); //创建学生对象
        Student s2 = new Student("lisi",27);
        Student s3 = new Student("wangwu",29);
        ts.add(s1);	//把学生添加到集合
        ts.add(s2);
        ts.add(s3);
        for (Student student : ts) {  		//遍历集合
            System.out.println(student);
        }
    }
}

定制排序:
public class Teacher {
    private String name;
    private int age;
    public Teacher() {}
    public Teacher(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "Teacher{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
public class MyTreeSet4 {
    public static void main(String[] args) {
      	//创建集合对象
        TreeSet ts = new TreeSet(new Comparator() {
            @Override
            public int compare(Teacher o1, Teacher o2) {
                //o1表示现在要存入的那个元素
                //o2表示已经存入到集合中的元素
                //主要条件
                int result = o1.getAge() - o2.getAge();
                //次要条件
                result = result == 0 ? o1.getName().compareTo(o2.getName()) : result;
                return result;
            }
        });
        Teacher t1 = new Teacher("zhangsan",23);	//创建老师对象
        Teacher t2 = new Teacher("lisi",22);
        Teacher t3 = new Teacher("wangwu",24);
        Teacher t4 = new Teacher("zhaoliu",24);
        ts.add(t1);	//把老师添加到集合
        ts.add(t2);
        ts.add(t3);
        ts.add(t4);
        for (Teacher teacher : ts) {	//把老师添加到集合
            System.out.println(teacher);
        }
    }
}

(5) Map不同实现类的对比与HashMap中元素的特点

现实生活与开发中,我们常会看到这样的一类集合:用户ID与账户信息、学生姓名与考试成绩、IP地址与主机名等,这种一一对应的关系,就称作映射。Java提供了专门的集合框架用来存储这种映射关系的对象,即java.util.Map接口。

Map及其实现类对比
    java.util.Map:存储一对一对的数据(key-value键值对,(x1,y1)、(x2,y2)-->y=f

  (x),类似于高中的函数)

          |----HashMap:主要实现类;线程不安全的,效率高;可以添加null的key和value值;

                                   底层使用数组+单向链表+红黑树结构存储(jdk8)
                 l----LinkedHashMap:是HashMap的子类;在HashMap使用的数据结构的基础

                                                   上,增加了一对双向链表,用于记录添加的元素的先后顺

                                                    序,进而我们在追历元素时,就可以按照添加的顺序显示。
                                                    开发中,对于频繁的遍历操作,建议使用此类。
          l----TreeMap:底层使用红黑树存储;可以按照添加的key-value中的key元素的指定的

                                 属性的大小顺序进行遍历。需要考虑使用①自然排序②定制排序。

          |----Hashtable:古老实现类;线程安全的,效率低;可以添加null的key或value值;底

                                   层使用数组+单向链表结构存储(jdk8)

                |----Properties:其key和value都是String类型。常用来处理属性文件。

HashMap中元素的特点:
   1️⃣HashMap中的所有的key彼此之间是不可重复的、无序的。所有的key就构成一个Set集

        合。--->key所在的类要重写hashCode()和equals()

   2️⃣HashMap中的所有的value彼此之间是可重复的、无序的。所有的value就构成一个

        Collection集合。--->value所在的类要重写equals()
   3️⃣HashMap中的一个key-value,就构成了一个entry。HashMap中的所有的entry彼此之间

        是不可重复的、无序的。所有的entry就构成了一个Set集合。

 (6)Map接口常用方法:

添加、修改操作:
- Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
- void putAll(Map m):将m中的所有key-value对存放到当前map中
删除操作:
- Object remove(Object key):移除指定key的key-value对,并返回value
- void clear():清空当前map中的所有数据
元素查询的操作:
- Object get(Object key):获取指定key对应的value
- boolean containsKey(Object key):是否包含指定的key
- boolean containsValue(Object value):是否包含指定的value
- int size():返回map中key-value对的个数
- boolean isEmpty():判断当前map是否为空
- boolean equals(Object obj):判断当前map和参数对象obj是否相等
元视图操作的方法:
- Set keySet():返回所有key构成的Set集合
- Collection values():返回所有value构成的Collection集合
- Set entrySet():返回所有key-value对构成的Set集合

例如:
        Map map = new HashMap();
        map.put("tom",23);
        map.put("jack",22);
        map.put("liming",25);
        System.out.println(map);//{tom=23, liming=25, jack=22}
        Object value= map.remove("tom");
        System.out.println(value);//23
        System.out.println(map); //{liming=25, jack=22}
        map.put("jack",19);
        System.out.println(map);//{liming=25, jack=19}
        //遍历key
        Set keySet = map.keySet();
        Iterator iterator=keySet.iterator();
        while (iterator.hasNext()){
            Object key=iterator.next();
            System.out.println(key);
        }
        //遍历value
        Collection values = map.values();
        for(Object key:values){
            System.out.println(key);
        }
        //遍历entry集
        Set entrySet = map.entrySet();
        Iterator iterator1=entrySet.iterator();
        while (iterator1.hasNext()){
            System.out.println(iterator1.next());
        }

 (7)Treemap和Properties的使用

TreeMap的使用:

    底层使用红黑树存储;
    可以按照添加的key-value中的key元素的指定的属性的大小顺序进行遍历。

    需要考虑使用①自然排序②定制排序。
    要求:向TreeMap中添加的key必须是同一个类型的对象。

Hashtable与Properties的使用:

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

(8)Collections工具类的使用

Collections是一个操作Set、List和Map等集合的工具类。

排序操作:
- reverse(List):反转 List 中元素的顺序
- shuffle(List):对 List 集合元素进行随机排序
- sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
- sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
- swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换

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

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

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

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

16. 泛型 

(1)泛型的理解及其在集合、比较器中的使用

什么是泛型?
所谓泛型,就是允许在定义类、接口时通过一个‘标识’、表示类中某个属性的类型、或者是某个方法的返回值或参数的类型。这个类型参数将在使用时(例如,继承或实现这个接口、创建对象或调用方法时)确定(即传入实际的类型参数,也称为类型实参)。

在集合中使用泛型之前可能存在的问题:

例如:
List list = new ArrayList();
list.add(2);
list.add(33);
list.add(55);
list.add("Tom");
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
    Integer score =(Integer) iterator.next(); 转换Tom时转换异常报错
    int i=score;  
    System.out.println(i);   
}

问题1:类型不安全。因为add()的参数是0bject类型,意味着任何类型的对象都可以添加成功
问题2:需要使用强转操作,繁琐。还有可能导致ClassCastException异常。

在集合,比较器中使用泛型:

泛型在List中的使用:   
List<Integer> list=new ArrayList<Integer>();
list.add(2);
list.add(33);
list.add(55);
//list.add("Tom");当添加非Integer类型数据时,编译不通过
Iterator<Integer> iterator=list.iterator();
while (iterator.hasNext()){
      Integer score = iterator.next();//不需要强转,直接可以获取添加时的元素的数据类型
      int i=score;
      System.out.println(i);
}

泛型在Map中的使用:
HashMap<String,Integer> map = new HashMap<>();后面这个尖括号中可省略不写,叫类型推断
map.put("Tom",67);
map.put("Jim",56);
map.put("Rose",88);
//编译不通过
//map.put(67,"Jack");
//遍历key集
Set<String> keySet = map.keySet();
for(String str:keySet){
    System.out.println(str);
}
//遍历value集
Collection<Integer> values = map.values();
Iterator<Integer> iterator = values.iterator();
while(iterator.hasNext()){
      Integer value = iterator.next();
      System.out.println(value);
}
//遍历entry集
Set<Map.Entry<String, Integer>> entrySet = map.entrySet();
jdk10之后可写成 var entrySet = map.entrySet();
Iterator<Map.Entry<String, Integer>> iterator1 = entrySet.iterator();
jdk10之后可写成 var iterator1 = entrySet.iterator();
while(iterator1.hasNext()){
      Map.Entry<String, Integer> entry = iterator1.next();
      String key = entry.getKey();
      Integer value = entry.getValue();
      System.out.println(key + ":" + value);
}

使用说明:
集合框架在声明接口和其实现类时,使用了泛型(jdk5.0),在实例化集合对象时,如果没有使用泛型,则认为操作的是0bject类型的数据。如果使用了泛型,则需要指明泛型的具体类型。一旦指明了泛型的具体类型,则在集合的相关的方法中,凡是使用类的泛型的位置,都替换为具体的泛型类型。

(2)自定义泛型类和方法:

声明类或接口时,在类名或接口名后面声明泛型类型,我们把这样的类或接口称为`泛型类`或`泛型接口`。
[修饰符] class 类名<类型变量列表> [extends 父类] [implements 接口们]{}
[修饰符] interface 接口名<类型变量列表> [implements 接口们]{}
例如:
  public class A<T>{}
  public interface B<T1,T2>{}

说明:
① 我们在声明完自定义泛型类以后,可以在类的内部(比如:属性、方法、构造器中)使用类的泛型。
② 我们在创建自定义泛型类的对象时,可以指明泛型参数类型。一旦指明,内部凡是使用类的泛型参数的
  位置,都具体化为指定的类的泛型类型。
③ 如果在创建自定义泛型类的对象时,没有指明泛型参数类型,那么泛型将被擦除,泛型对应的类型均按
  照Object处理,但不等价于Object。
经验:泛型要使用一路都用。要不用,一路都不要用。
④ 泛型的指定中必须使用引用数据类型。不能使用基本数据类型,此时只能使用包装类替换。
⑤ 除创建泛型类对象外,子类继承泛型类时、实现类实现泛型接口时,也可以确定泛型结构中的泛型参数。 
如果我们在给泛型类提供子类时,子类也不确定泛型的类型,则可以继续使用泛型参数。
我们还可以在现有的父类的泛型参数的基础上,新增泛型参数。

注意:
① 泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:<E1,E2,E3>
② JDK7.0 开始,泛型的简化操作:ArrayList<Fruit> flist = new ArrayList<>();
③ 如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。
④ 不能使用new E[]。但是可以:E[] elements = (E[])new Object[capacity];
​  参考:ArrayList源码中声明:Object[] elementData,而非泛型参数类型数组。
⑤ 在类/接口上声明的泛型,在本类或本接口中即代表某种类型,但不可以在静态方法中使用类的泛型。
⑥ 异常类不能是带泛型的。

自定义泛型方法:
  问题:在泛型类的方法中,使用了类的泛型参数。那么此方法是泛型方法吗?
  格式:
    权限修饰符 <T> 返回值类型 方法名(形参列表){
        //通常在形参列表或返回值类型的位置会出现泛型参数T
    }
  例如:java.util.Arrays类中的
       public static <T> List<T> asList(T... a){....}
  举例:
    public <E> E method(E e){}
  说明:
    声明泛型方法时,一定要添加泛型参数<T>
    泛型参数在方法调用时,指明其具体的类型
    泛型方法可以根据需要声明为static
    泛型方法所属的类是否是一个泛型类,都可以。

import org.junit.jupiter.api.Test;
public class CallableTest {
    public static void main(String[] args) {}
    @Test
    public void test() {
        //实例化时,就可以指明类的泛型参数的类型
        Order order = new Order();
        Object obj = order.getT();
        //泛型参数在指明时,是不可以使用基本数据类型的!但是可以使用包装类替代基本数据类型。
        //Order<int> order1=new Order<int>(); 报错
        //在实例化时,可以指明类的泛型参数的具体类型!一旦指明了泛型的类型,则在泛型类中凡是
        //使用泛型参数的位置,都替换为指定的类型。
        Order<Integer> order2 = new Order<>();
        Integer t = order2.getT();
        Order<String> order3 = new Order<>();
        order3.setT("aa");
    }
    @Test
    public void test2() {
        SubOrder sub1 = new SubOrder();
        //SubOrder<Integer> sub2=new SubOrder<>();因为Sub0rder1不是泛型类,此处编译错误
        Object t = sub1.getT();
    }

    @Test
    public void test3() {
        SubOrder2 sub1 = new SubOrder2();
        Integer t = sub1.getT();
        SubOrder3<String> sub2 = new SubOrder3<>();
        String t1 = sub2.getT();
        sub2.show("22");
        SubOrder4<String> sub3 = new SubOrder4<>();
        Integer t2 = sub3.getT();
        String e = sub3.getE();
        SubOrder5<String, Integer> sub4 = new SubOrder5<>();
        String t3 = sub4.getT();
        Integer e1 = sub4.getE();
    }
}
class Order<T> {
    //声明了类的泛型参数以后,就可以在类的内部使用此泛型参数。
    T t;
    int orderId;
    public Order() {}
    public Order(T t, int orderId) {
        this.t = t;
        this.orderId = orderId;
    }
    public T getT() {
        return t;
    }
    public void setT(T t) {
        this.t = t;
    }
    public int getOrderId() {
        return orderId;
    }
    public void setOrderId(int orderId) {
        this.orderId = orderId;
    }
    @Override
    public String toString() {
        return "Order{" + "t=" + t + ",orderId=" + orderId + '}';
    }
}
//SubOrder1不是泛型类
class SubOrder extends Order { }
//SubOrder2不是泛型类
class SubOrder2 extends Order<Integer> { }
//SubOrder3是泛型类
class SubOrder3<T> extends Order<T> {
    public void show(T t) {
        System.out.println(t);
    }
}
//SubOrder4是泛型类
class SubOrder4<E> extends Order<Integer> {
    E e;
    public SubOrder4() {}
    public SubOrder4(E e) {}
    public SubOrder4(Integer integer, int orderId, E e) {
        super(integer, orderId);
        this.e = e;
    }
    public E getE() {
        return e;
    }
    @Override
    public void setT(Integer integer) {
        super.setT(integer);
    }
}
//SubOrder5是泛型类
class SubOrder5<T, E> extends Order<T> {
    E e;
    public SubOrder5() {
    }
    public SubOrder5(T t, int orderId, E e) {
        super(t, orderId);
        this.e = e;
    }
    public E getE() {
        return e;
    }
    @Override
    public void setT(T t) {
        super.setT(t);
    }
}

 (3)通配符的使用:

通配符:?  允许所有泛型的引用调用

举例:ArrayList<?>

G<?>可以看做是G<A>类型的父类,即可以将G<A>的对象赋值给G<>类型的引用(或变量)

读写数据的特点(以集合ArrayList<?>为例说明)

    读取数据:允许的,读取的值的类型为0bject类型

    写入数据:不允许的。特例:写入null值

有条件限制的通配符

List<?extends A>:可以将List<A>或List<B>赋值给List<?extends A>。其中B类是A类的子类。

List <?super  A>:可以将List<A>或List<B>赋值给List<?extends A>。其中B类是A类的父类。

有限制条件的统配符的读写操作(难、了解)

   技巧:开发中,遇到了带限制条件的通配符,在赋值时,如果没报错,那就正常使用。

              如果报错了,知道不能这样写。改改

17. 数据结构与集合源码

(1) 数据结构剖析

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

具体研究对象如下:

研究对象一:数据间逻辑关系

数据的逻辑结构指反映数据元素之间的逻辑关系,而与数据的存储无关,是独立于计算机的。

   • 集合结构:数据结构中的元素之间除了“同属一个集合” 的相互关系外,别无其他关系。集

     合元素之间没有逻辑关系。

   • 线性结构:数据结构中的元素存在一对一的相互关系。比如:排队。结构中必须存在唯一

     的首元素和唯一的尾元素。体现为:一维数组、链表、栈、队列

   • 树形结构:数据结构中的元素存在一对多的相互关系。比如:家谱、文件系统、组织架构

   • 图形结构:数据结构中的元素存在多对多的相互关系。比如:全国铁路网、地铁图

研究对象二:数据的存储结构(或物理结构)

数据的物理结构/存储结构:包括数据元素的表示和关系的表示。数据的存储结构是逻辑结构用计算机语言的实现,它依赖于计算机语言。

   结构 1:顺序结构

                 • 顺序结构就是使用一组连续的存储单元依次存储逻辑上相邻的各个元素。

                 • 优点: 只需要申请存放数据本身的内存空间即可,支持下标访问,也可以实现

                               随机访问。

                 • 缺点: 必须静态分配连续空间,内存空间的利用率比较低。插入或删除可能需

                               要移动大量元素,效率比较低

   结构2:链式结构

                • 不使用连续的存储空间存放结构的元素,而是为每一个元素构造一个节点。节点

                  中除了存放数据本身以外,还需要存放指向下一个节点的指针。

                • 优点:不采用连续的存储空间导致内存空间利用率比较高,克服顺序存储结构中

                            预知元素个数的缺点。插入或删除元素时,不需要移动大量的元素。

                • 缺点:需要额外的空间来表达数据之间的逻辑关系,不支持下标访问和随机访问

   结构3:索引结构

                • 除建立存储节点信息外,还建立附加的索引表来记录每个元素节点的地址。索引

                  表由若干索引项组成。索引项的一般形式是:(关键字,地址)。

                • 优点:用节点的索引号来确定结点存储地址,检索速度快。

                • 缺点: 增加了附加的索引表,会占用较多的存储空间。在增加和删除数据时要修

                              改索引表,因而会花费较多的时间

   结构4:散列结构

                • 根据元素的关键字直接计算出该元素的存储地址,又称为 Hash 存储。

                • 优点:检索、增加和删除结点的操作都很快。

                • 缺点:不支持排序,一般比用线性表存储需要更多的空间,并且记录的关键字不

                             能重复。

    开发中,我们更习惯上如下的方式理解存储结构:
                >线性表(一对一关系):一维数组、单向链表、双向链表、栈、队列
                >树(一对多关系):各种树。比如:二叉树、B+树
                >图(多对多关系)
                >哈希表:比如:HashMap、HashSet

​​​​​​​研究对象三:运算结构

施加在数据上的运算包括运算的定义和实现。运算的定义是针对逻辑结构的, 指出运算的功能;运算的实现是针对存储结构的,指出运算的具体操作步骤。

   • 分配资源,建立结构,释放资源

   • 插入和删除

   • 获取和遍历

   • 修改和排序

(2) 常用的数据结构

一维数组:

在 Java 中,数组是用来存放同一种数据类型的集合,注意只能存放同一种数据类型。

//只声明了类型和长度
数据类型[] 数组名称 = new 数据类型[数组长度];
//声明了类型,初始化赋值,大小由元素个数决定
数据类型[] 数组名称 = {数组元素 1,数组元素 2,......}

例如:整型数组

例如:对象数组

物理结构特点:

          – 申请内存:一次申请一大段连续的空间,一旦申请到了,内存就固定了。

          – 不能动态扩展(初始化给大了,浪费;给小了,不够用),插入快,删除和查找慢。

          – 存储特点:所有数据存储在这个连续的空间中,数组中的每一个元素都是一个具体的

             数据(或对象),所有数据都紧密排布,不能有间隔。

具体的,如下图:


链表:

链表的特点:

   • 逻辑结构:线性结构

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

   • 存储特点:链表由一系列结点 node(链表中每一个元素称为结点)组成,结点可以在代

     码执行过程中动态创建。每个结点包括两个部分:一个是存储数据元素的数据域,另一个

     是存储下一个结点地址的指针域。

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

单项链表:
class Node{
    Object data;
    Node next;
    public Node(Object data){
        this.data = data;
    }
}
Node node1=new Node("aa");
Node node2=new Node("bb");
node1.next=node2;

双向链表:
class Node{
   Node prev;
   Object data;
   Node next;
   public  Node(Object data){
       this.data = data;
   }
   public  Node(Node prev,Object data,Node next){
       this.prev = prev;
       this.data = data;
       this.next = next;
   }
}
Node node1=new Node(null,"aa",null);
Node node2=new Node(node2,"bb",null);
Node node3=new Node(node2,"cc",null);
node1.next=node2;
node2.next=node3;

具体的,如下图:


栈:

栈的特点:

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

    • 栈按照先进后出(FILO,first in last out)的原则存储数据,先进入的数据被压 、入栈底,最

       后的数据在栈顶。每次删除(退栈)的总是删除当前栈中最后插入(进栈)的元素,而

       最先插入的是被放在栈的底部,要到最后才能删除。

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

         – Stack 就是顺序栈,它是 Vector 的子类。

         – LinkedList 是链式栈。

   • 体现栈结构的操作方法:

        – peek()方法:查看栈顶元素,不弹出

        – pop()方法:弹出栈

class Stack {
    Object[] values;
    int size;
    public Stack(int length) {
        values = new Object[length];
    }
    //入栈
    public void push(Object ele, int index) {
        if (size == values.length) {
            throw new RuntimeException("栈空间已满,入栈失败");
        }
        values[size] = ele;
        size++;
    }
    //出栈
    public  Object pop() {
        if(size<=0){
                throw new RuntimeException("栈空间已空,出栈失败");
        }
        Object obj=values[size-1];
        values[size-1]=null;
        size--;
        return obj;
    }
}

        – push(E e)方法:压入栈 

   • 时间复杂度:

        – 索引: O(n)

        – 搜索: O(n)

        – 插入: O(1)

        – 移除: O(1)

   • 图示:

具体如图:


队列:

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

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

   • 队列的修改原则:队列的修改是依先进先出(FIFO)的原则进行的。新来的成员总是加

     入队尾(即不允许"加塞"),每次离开的成员总是队列头上的(不允许中途离 队),即当

     前"最老的"成员离队。

   • 图示:

class Queue {
    Object[] values;
    int size;//记录存储的元素的个数
    public Queue(int length) {
        values = new Object[length];
    }
    public void add(Object ele) { //添加
        if (size == values.length) {
            throw new RuntimeException("队列已满,添加失败");
        }
        values[size] = ele;
        size++;
    }
    public Object get(){ //获取
        if(size <+0) {
            throw new RuntimeException("队列已空,获取失败");
        }
        Object obj = values[0];
        //数据前移
        for (int i = 0; i < size-1; i++) {
            valuse[i] = values[i+1];
        }
        //最后一个元素置空
        values[size-1]=null;
        size--;
        return obj;
    }
}

如图所示:


树与二叉树:

树的理解:

专有名词解释:

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

       根节点:最上面的结点称之为根,一颗树只有一个根且由根发展而来,从另外一个角度

                     来说,每个结点都可以认为是其子树的根

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

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

                      节点

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

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

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

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

                                                       B、C、E、G 都是

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

       结点的层数:从根节点到树中某结点所经路径上的分支树称为该结点的层数,根节点的

                            层数规定为 1,其余结点的层数等于其父亲结点的层数+1

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

二叉树的基本概念:

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

class TreeNode{
    TreeNode left;
    Object data;
    TreeNode right;
    public TreeNode(TreeNode left, Object data, TreeNode right) {
        this.left = left;
        this.data = data;
        this.right = right;
    }
    public TreeNode(Object data) {
        this.data = data;
    }
}
TreeNode node=new TreeNode(null,"aa",null);
TreeNode leftNode=new TreeNode(null,"bb",null);
TreeNode rightNode=new TreeNode(null,"cc",null);
node1.left=leftNode;
node1.right=rightNode;

二叉树的遍历:

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

                    即先访问根结点,再前序遍历左子树,最后再前序遍历右子树。前序遍历运算

                    访问二叉树各结点是以根、左、右的顺序进行访问的。

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

                    即先中前序遍历左子树,然后再访问根结点,最后再中序遍 历右子树。中序遍

                    历运算访问二叉树各结点是以左、根、右的顺序进行访问的。

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

                    即先后序遍历左子树,然后再后序遍历右子树,最后访问根 结点。后序遍历运

                    算访问二叉树各结点是以左、右、根的顺序进行访问的。

前序遍历:ABDHIECFG

中序遍历:HDIBEAFCG

后序遍历:HIDEBFGCA

经典二叉树:

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

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

      左侧。

3、二叉排序/查找/搜索树:即为 BST (binary search/sort tree)。满足如下性质:

   (1)若它的左子树不为空,则左子树上所有结点的值均小于它的根节点的值;

   (2)若它的右子树上所有结点的值均大于它的根节点的值;

   (3)它的左、 右子树也分别为二叉排序/查找/搜索树。

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

4、平衡二叉树:(Self-balancing binary search tree,AVL)首先是二叉排序树,此外具有

      以下性质:

   (1)它是一棵空树或它的左右两个子树的高度差的 绝对值不超过 1

   (2)并且左右两个子树也都是一棵平衡二叉树

   (3)不要求非叶节点都有两个子结点

         平衡二叉树的目的是为了减少二叉查找树的层次,提高查找速度。

         平衡二叉树的常用实现有红黑树、AVL、替罪羊树、Treap、伸展树等。

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

     红(Red)或黑(Black)。

     红黑树是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,它 是在 1972

     年由 Rudolf Bayer 发明的。红黑树是复杂的,但它的操作有着良好的最坏情况运行时

     间,并且在实践中是高效的:它可以在 O(log n)时间内做查 找,插入和删除, 这里的 n

     是树中元素的数目。

     红黑树的特性:

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

         • 根节点是黑色

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

           子节点)

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

            连续的红色节点)

         • 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点(确保没有一条路径

           会比其他路径长出 2 倍)

  当我们插入或删除节点时,可能会破坏已有的红黑树,使得它不满足以上 5 个 要求,那

  么此时就需要进行处理,使得它继续满足以上的 5 个要求:

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

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

  红黑树可以通过红色节点和黑色节点尽可能的保证二叉树的平衡。主 要是用它来存储有序

  的数据,它的时间复杂度是 O(logN),效率非常之高。

(3)ArrayList在JDK7和JDK8中的源码解析

ArrayList的特点:
  >实现了List接口,存储有序的、可以重复的数据
  >底层使思0bject[]数组存储
  >线程不安全的

ArrayList源码解析:
  jdk7版本:(以jdk1.7.0_07为例)
  //如下代码的执行:底层会初始行数组,数组的长度为10。Object[] elementData=new 0bject[10];
  ArrayList<String> list = new Arraylist<>();
  list.add("AА");//elementData[0]="АА";
  list.add("BB");//elementData[1]="ВB";
  当要添加第11个元素的时候,底层的elementData数组已满,则需要扩容。
  默认扩容为原来长度的1.5倍。并将原有数组中的元素复制到新的数组中。

  jdk8版本:(以jdk1.8.0_271为例)
  //如下代码的执行:底层会初始化数组,即:0bject[] elementData = new Object[]{};
  ArrayList<String> list = new ArrayList<>();
  list.add("AA");//首次添加元素时,会初始化数组elementData = new Object[10]; 
  elementData[0]="AA";
  list.add("BB");//eLementData[1]="BB";
  当要添加第11个元素的时候,底层的eementData数组已满,则需要扩容。默认扩容为原来长度的1.5倍。
  并将原有数组中的元素复制到新的数组中。

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

(4)Vector和LinkedList在JDK8中的源码解析

Vector的特点:
  >实现了List接口,存储有序的、可以重复的数据
  >底层使用0bject[]数组存储
  >线程安全的

Vector源码解析:(以jdk1.8.0_271为例)
  Vector v= new Vector();//底层初始化数组,长度为10 Object[] elementData = new Object[10];
  v.add("AА");//elementData[0]="АА";
  v.add("BВ");//elementData[1]="ВB";
  当添加第11个元素时,需要扩容。默认扩容为原来的2倍。

LinkedList的特点:
  >实现了List接口,存储有序的、可以重复的数据
  >底层使用双向链表存储
  >线程不安全的

LinkedList在jdk8中的源码解析:
  LinkedList<String> list = new LinkedList<>();//底层啥也没做
  list.add("AA");//将"AA"封装到一个Node对象1中,list对象的属性first、last都指向此Node
                     对象1
  list.add("BB");//将"BB"封装到一个Node对象2中,对象1和对象2构成一个双向链表,同时Last
                   //指向此Node对象2       
  因为LinkedList使用的是双向链表,不需要考虑扩容问题。

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

启示与开发建议:
  1. Vector基本不使用了。
  2. ArrayList底层使用数组结构,查找和添加(尾部添加)操作效率高,时间复杂度为o(1)
                               删除和插入操作效率低,时间复杂度为o(n)
     LinkedList底层使用双向链表结构,删除和插入操作效率高,时间复杂度为o(1)
                                   查找和添加(尾部添加)操作效率高,时间复杂度为o(n)
                                  (有可能添加操作是o(1))
  3。在选择了ArrayList的前提下,new ArrayList():底层创建长度为10的数组。
                              new ArrayList(int capacity):底层创建指定capacity长度
                                                             的数组。
     如果开发中,大体确认数组的长度,则推荐使用ArrayList(int capacity)这个构造器,避免
     了底层的扩容、复制数组的操作

(5)HashMap在JDK7和JDK8中的源码解析

HashMap中元素的特点
   > HashMap中的所有的key彼此之间是不可重复的、无序的。所有的key就构成一个Set集合。
     --->key所在的类要重写hashCode()和equals()
   > HashMap中的所有的value彼此之间是可重复的、无序的。所有的value就构成一个Collection集合。
     --->value所在类要重写equals()
   > HashMap中的一个key-value,就构成了一个entryl.
   > HashMap中的所有的entry彼此之间是不可重复的、无序的。所有的entry就构成了一个Set集合。

HashMap源码解析: 
jdk7中创建对象和添加数据过程(以JDK1.7.0_07为例说明):
  //创建对象的过程中,底层会初始化数组Entry[] table =new Entry[16];
  HashMap<String,Integer> map = new HashMap<>();
  map.put("AA",78);//"AA"和78封装到一个Entry对象中,考虑将此对象添加到table数组中。
  添加/修改的过程:
    将(key1,value1)添加到当前的map中:
    首先,需要调用key1所在类的hashCode()方法,计算key1对应的哈希值1,此哈希值1经过某种算法
   (hash())之后,得到哈希值2。
    哈希值2再经过某种算法(indexFor())之后,就确定了(key1,value1)在数组table中的索引位
    置i
    1.1如果此索引位置1的数组上没有元素,则(key1,value1)添加成功。---->情况1
    1.2如果此索引位置i的数组上有元素(key2,value2),则需要继续比较key1和key2的哈希值2
       --->哈希钟突
    2.1如果key1的哈希值2与key2的哈希值2不相同,则(key1,value1)添加成功。---->情况2
    2.2如果key1的哈希值2与key2的哈希值2相同,则需要继续比较key1和key2的equals()。
       要调用key1所在类的equals(),将key2作为参数传递进去。
    3.1调用equals(),返回false:则(key1,value1)添加成功。--->情况3
    3.2调用equals(),返回true:则认为key1和key2是相同的。默认情况下,value1替换原有的value2。
  说明:情况1:将(key1,value1)存放到数组的索引i的位置
       情况2,情况3:(key1,value1)元素与现有的(key2,value2)构成单向链表结构,
                    (key1,value1)指向(key2,value2)
  随着不断的添加元素,在满足如下的条件的情况下,会考虑扩容(size >= threshold)&&(null!= 
  table[i])
  当元素的个数达到临界值(->数组的长度*加载因子)时,就考虑扩容。默认的临界值= 16*0.75-->12
  默认扩容为原来的2倍。

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

(6)LinkedHashMap与HashSet与LinkedHashSet的源码解析

LinkedHashMap与HashMap的关系:
  > LinkedHashMap是HashMap的子类
  > LinkedHashMap在HashMap使用的数组+单向链表+红黑树的基础上,又增加了一对双向链表,
    记录添加的(key,value)的先后顺序。便于我们追历所有的key-value

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

底层结构:LinkedHashMap内部定义了一个Entry
static class Entry<K,V> extends HashMap.Node<K,V>{
   Entry<K,V> before,after;
   Entry(int hash,K key,V value,Node<K,V> next){
       super(hash,key,value,next);
   }
}

HashSet和LinkedHashSet的源码分析
  > HashSet底层使用的是HashMap
  > LinkedHashSet底层使用的是LinkedHashMap

18. File类与IO流

(1)File类的实例化与常用方法

File类的理解:
      File类位于java.io包下,本章中涉及到的相关流也都声明在java.io包下。

      File类的一个对象,对应与操作系统下的一个文件或一个文件目录(或文件夹)

      File 能新建、删除、重命名文件和目录,但 File 不能访问文件内容本身。如果需要访问

      文件内容本身,则需要使用输入/输出流。

      File类的对象,通常是作为io流操作的文件的端点出现的

      File对象可以作为参数传递给流的构造器。

      想要在Java程序中表示一个真实存在的文件或目录,那么必须有一个File对象,但是Java

      程序中的一个File对象,可能没有一个真实存在的文件或目录。

构造器:

绝对路径:以windows操作系统为例,包括盘符在内的文件或文件目录的完整路径。
相对路径:相对于某一个文件目录来讲的相对的位置。
IDEA中,如果使用单元测试方法:相对于当前的module来讲 
       如果使用main()方法:相对于当前的project来讲

public File(String pathname):以pathname为路径创建File对象,可以是绝对路径或者相对路
                              径,如果pathname是相对路径,则默认的当前路径在系统属性
                              user.dir中存储。
public File(String parent, String child):以parent为父路径,child为子路径创建File
                                          对象
public File(File parent, String child):根据一个父File对象和子文件路径创建File对象

例如:
  File file1=new File("d:\\io\\hello.txt");
  File file2=new File("abc");
  System.out.println(file2.getAbsolutePath());
  //参数1:一定是一个文件目录 参数2:可以是一个文件,也可以是一个文件目录
  File file3=new File("d:\\io","hello.txt");
  File file4=new File("abc","a12");
  //参数1:一定是一个文件目录  参数2:可以是一个文件,也可以是一个文件目录
  File file5=new File(file4,"ab.txt");

注意:
  1️⃣无论该路径下是否存在文件或者目录,都不影响File对象的创建。
  2️⃣window的路径分隔符使用“\”,而Java程序中的“\”表示转义字符,所以在Windows中表示路径,
    需要用“\\”。或者直接使用“/”也可以,Java程序支持将“/”当成平台无关的`路径分隔符`。

常用方法:

获取文件和目录基本信息:
- public String getName() :获取名称
- public String getPath() :获取路径
- public String getAbsolutePath():获取绝对路径
- public File getAbsoluteFile():获取绝对路径表示的文件
- public String getParent():获取上层文件目录路径。若无,返回null
- public long length() :获取文件长度(即:字节数)。不能获取目录的长度。
- public long lastModified() :获取最后一次的修改时间,毫秒值
列出目录的下一级:
- public String[] list() :返回一个String数组,表示该File目录中的所有子文件或目录。
- public File[] listFiles() :返回一个File数组,表示该File目录中的所有的子文件或目录。
File类的重命名功能:
- public boolean renameTo(File dest):把文件重命名为指定的文件路径。
判断功能的方法
- public boolean exists():此File表示的文件或目录是否实际存在。
- public boolean isDirectory():此File表示的是否为目录。
- public boolean isFile() :此File表示的是否为文件。
- public boolean canRead():判断是否可读
- public boolean canWrite():判断是否可写
- public boolean isHidden():判断是否隐藏
创建、删除功能:
- public boolean createNewFile():创建文件。若文件存在,则不创建,返回false。
- public boolean mkdir():创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录
                          的上层目录不存在,也不创建。
- public boolean mkdirs():创建文件目录。如果上层文件目录不存在,一并创建。
- public boolean delete():删除文件或者文件夹
  删除注意事项:① Java中的删除不走回收站。② 要删除一个文件目录,请注意该文件目录内不能包
  含文件或者文件目录。

 (2)IO流概述与流的分类

Java IO原理:

Java程序中,对于数据的输入/输出操作以“流(stream)” 的方式进行,可以看做是一种数据的流动。

I/O流中的I/O是Input/Output的缩写, I/O技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等。

  • 输入input:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中。

  • 输出output:将程序(内存)数据输出到磁盘、光盘等存储设备中。


流的分类:

java.io包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据。

   按数据的流向不同分为:输入流输出流

        输入流 :把数据从其他设备上读取到内存中的流。

                        以InputStream、Reader结尾

        输出流 :把数据从内存 中写出到其他设备上的流。

                       以OutputStream、Writer结尾

   按操作数据单位的不同分为:字节流(8bit)字符流(16bit)

        字节流 :以字节为单位,读写数据的流。

                        以InputStream、OutputStream结尾

        字符流 :以字符为单位,读写数据的流。

                       以Reader、Writer结尾

   根据IO流的角色不同分为:节点流处理流

        节点流:直接从数据源或目的地读写数据

        处理流:不直接连接到数据源或目的地,而是“连接”在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更为强大的读写功能


流的API:

Java的IO流共涉及40多个类,实际上非常规则,都是从如下4个抽象基类派生的

(抽象基类)输入流输出流
字节流InputStreamOutputStream
字符流ReaderWriter

由这四个类派生出来的子类名称都是以其父类名作为子类名后缀

常用的节点流:  

  • 文件流: FileInputStream、FileOutputStrean、FileReader、FileWriter

  • 字节/字符数组流: ByteArrayInputStream、ByteArrayOutputStream、CharArrayReader、CharArrayWriter

    • 对数组进行处理的节点流(对应的不再是文件,而是内存中的一个数组)。

常用处理流:

  • 缓冲流:BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter

    • 作用:增加缓冲功能,避免频繁读写硬盘,进而提升读写效率。

  • 转换流:InputStreamReader、OutputStreamReader

    • 作用:实现字节流和字符流之间的转换。

  • 对象流:ObjectInputStream、ObjectOutputStream

    • 作用:提供直接读写Java对象功能

(3)使用FileReader和FileWriter读取、写出文本数据

FileReader \ FileWriter 的使用:
执行步骤:
     第1步:创建读取或写出的File类的对象
     第2步:创建输入流或输出流
     第3步:具体的读入或写出的过程。
                  读入:read(char[]  cbuffer)

                  写出:write(String str)/ write(char[]  cbuffer,0,len)

     第4步:关闭流资源,避免内存泄漏

注意点:
    ①因为涉及到流资源的关闭操作,所以出现异常的话,需要使用try-catch-finally的方式来

        处理异常

    ②对于输入流来讲,要求File类的对象对应的物理磁盘上的文件必须存在。否则,会报

        FileNotFoundException对于输出流来讲,File类的对象对应的物理磁盘上的文件可以不

        存在。
        如果此文件不存在,则在输出的过程中,会自动创建此文件,并写出数据到此文件中。
        如果此文件存在,使用FileWriter(File file)或FileWriter(File file,false):输出数据

        过程中,会新建同名的文件对现有的文件进行覆盖。FileWriter(File file,true):输出

        数据过程中,会在现有的文件的末尾追加写出内容

例如:

读取hello.txt中的内容,显示在控制台上。
   public void test() {
        FileReader fr = null;
        try {
            //创建File类的对象,对应着abc.txt文件
            File file = new File("abc.txt");
            //创建输入型的字符流,用于读取数据
            fr = new FileReader(file);
            //读取数据,并显示在控制台上
            char[] buf = new char[(int) file.length()];
            int len = fr.read(buf);
            for (int i = 0; i < len; i++) {
                System.out.print(buf[i]);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            //流资源的关闭操作(必须要关闭,否则会内存泄漏)
            try {
                if (fr != null) {
                    fr.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

将内存中的数据写出到指定的文件中:
   public void test2() {
        FileWriter fw = null;
        try {
            //创建File类的对象,指明要写出的文件的名称
            File file = new File("abc.txt");
            //创建输出流
            //覆盖文件使用构造器 fw = new FileWriter(file);
            //在现有文件基础上追加内容使用的构造器 fw = new FileWriter(file,true);
            fw = new FileWriter(file);
            //写出的具体过程
            fw.write("i love you");
            fw.write("哈哈");
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (fw != null) {
                    fw.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

将内存中的数据写出到指定的文件中:
   public void test3() {
        FileReader fr = null;
        FileWriter fw = null;
        try {
            //创建File类的对象
            File srcFile = new File("abc.txt");
            File destFile = new File("abc_copy.txt");
            //创建输入流、输出流
            fr = new FileReader(srcFile);
            fw = new FileWriter(destFile);
            //数据的读入和写出的过程
            char[] cbbuffer=new char[5];
            int len;///记录每次读入到cbuffer中的字符的个数
            while ((len=fr.read(cbbuffer))!=-1){
                fw.write(cbbuffer,0,len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fr != null && fw != null) {
                try {
                    fw.close();
                    fr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

(4) FilelnputStream \ FileOutputStream的使用

执行步骤: 
    第1步:创建读取或写出的File类的对象

    第2步:创建输入流或输出流
    第3步:具体的读入或写出的过程。
                读入:read(byte[]  buffer)

                写出:write(byte[]  buffer,0,len)

    第4步:关闭流资源,避免内存泄漏

注意:  

   在上面注意点的基础之上,看其他的注意点。
   对于字符流,只能用来操作文本文件,不能用来处理非文本文件的。

   对于字节流,通常是用来处理非文本文件的、但是,如果涉及到文本文件的复制操作,也

   可以使用字节流

说明:
   文本文件:.txt、.java、.c、.cpp、.py等

   非文本文件:dоc、.xls、.jpg、.pdf、.mp3、.m4、.avi

 (5)缓冲流的使用

缓冲流的作用:提升文件读写的效率。

       4个缓冲流                                             使用的方法
处理非文本文件的字节流:
BufferedInputStream                            read(byte[] buffer)

Buffered0utputStream                          write(byte[] buffer,0,len)

处理文本文件的字符流:
BufferedReader                                    read(char[] cBuffer)/  readLine()
BufferedWriter                                      write(char[] cBuffer,0,len)

注意:readLine():每次读取一行文本中的数据。返回的字符申是不包含换行符的。|

实现的步骤:
    第1步:创建File的对象、流的对象(包括文件流、缓冲流)
    第2步:使用缓冲流实现读取数据或写出数据的过程(重点)
                 读取:int read(char[]cbuf/byte[]buffer):每次将数据读入到cbuf/buffer数组中,

                                                                                      并返回读入到数组中

                 写出:void write(String str)/write(char[]cbuf):将str或cbuf写出到文件中

                            void write(byte[]buffer)将byte[]写出到文件中

    第3步:关闭资源 

    例如:

字节流:
   public void test3() {
        FileInputStream fr = null;
        FileOutputStream fw = null;
        BufferedInputStream br=null;
        BufferedOutputStream bw=null;
        try {
            //创建File类的对象
            File srcFile = new File("play.jpg");
            File destFile = new File("play_copy.jpg");
            //创建相关的字节流,缓冲流
            fr = new FileInputStream(srcFile);
            fw = new FileOutputStream(destFile);
            br=new BufferedInputStream(fr);
            bw=new BufferedOutputStream(fw);
            //数据的读入和写出的过程
            byte[] cbbuffer=new byte[1024];
            int len;///记录每次读入到cbuffer中的字符的个数
            while ((len=fr.read(cbbuffer))!=-1){
                bw.write(cbbuffer,0,len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (br != null && fr != null) {
                try {
                    //关闭资源 外层的流的关闭
                    //由于外层流的关闭也会自动的对内层的流进行关闭操作。
                    //所以可以省略内层流的关闭。
                     bw.close();
                     br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

字符流:
    public void test3() {
        FileReader fr = null;
        FileWriter fw = null;
        BufferedReader br=null;
        BufferedWriter bw=null;
        try {
            //创建File类的对象
            File srcFile = new File("abc.txt");
            File destFile = new File("abc_copy.txt");
            //创建输入流、输出流
            fr = new FileReader(srcFile);
            fw = new FileWriter(destFile);
            br=new BufferedReader(fr);
            bw=new BufferedWriter(fw);
            //数据的读入和写出的过程
            String data;
            while ((data=br.readLine())!=null){
                bw.write(data);
                bw.newLine();//表示换行
                bw.flush();//刷新的方法。
                //每当调用此方法时,就会主动的将内存中的数据写出到磁盘文件中。
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fr != null && fw != null) {
                try {
                    bw.close();
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

(6)转换流以及各种字符集的使用:

字符编码:字符、字符串、字符数组--->字节、字节数组(从我们能看得懂的--->我们看不懂的)
字符解码:字节、字节数组--->字符、字符串、字符数组(从我们看不懂的--->我们能看得懂的)

如果希望程序在读取文本文件时,不出现乱码,需要注意什么?

解码时使用的字符集必须与当初编码时使用的字符集得相同。

拓展:解码集必须要与编码集兼容。比如:文件编码使用的是GBK,解码时使用的是utf-8。如果文件中只有abc等英文字符,此情况下也不会出现乱码。因为GBK和utf-8都向下兼容了ASCII(或ascii)

转换流:
    作用:实现字节与字符之间的转换

    API:InputStreamReader:将一个输入型的字节流转换为输入型的字符流。

             0utputStreamWriter:将一个输出型的字符流转换为输出型的字节流。

例如:

将gbk格式的文件转换为utf-8格式的文件存储。

//1.造文件
File file1=new File("abc.txt");
File file2=new File("abc_copy.txt");
//2.造流
FileInputStream fis=new FileInputStream(file1);
//参数2对应的是解码集,必须与abc_copy.txt的编码集一致。
InputStreamReader isr=new InputStreamReader(fis,"GBK");
FileOutputStream fos=new FileOutputStream(file2);
//参数2指明内存中的字符存储到文件中的节过程中使用的编码集。
OutputStreamWriter osw=new OutputStreamWriter(fos,"utf8");
//读写过程
char[] cBuffer=new char[1024];int len;
while ((len=isr.read(cBuffer))!=-1){
       osw.write(cBuffer,0,len);
}
System.out.println("操作完成");
//4.关闭资源
osw.close();
isr.close();

关于字符集的理解
    在存储的文件中的字符:
        ascii:主要用来存储a、b、c等英文字符和1、2、3、常用的标点符号。每个字符占用1

                   个字节。

        iso-8859-1:了解,每个字符占用1个字节。向下兼容ascii。

        gbk:用来存储中文简体繁体、a、b、c等英文字符和1、2、3、常用的标点符号等字

                 符。中文字符使用2个字节存储的。向下兼容ascii,意味着英文字符、1、2、3、

                 标点符号仍使用1个字节。

        utf-8:可以用来存储世界范围内主要的语言的所有的字符。使用1-4个不等的字节表示

                   一个字符。中文字符使用3个字节存储的。向下兼容ascii,意味着英文字符、1、

                   2、3、标点符号仍使用1个字节。

    在内存中的字符:
          一个字符(char)占用2个字节。在内存中使用的字符集称为Unicode字符集。

 (7)对象流的使用及对象的序列化机制

数据流及其作用:
Data0utputStream:可以将内存中的基本数据类型的变量、String类型的变量写出到具体的

                                 文件中。

DataInputStream:将文件中保存的数据还原为内存中的基本数据类型的变量、String类型的

                               变量。

对象流及其作用:
    API:ObjectInputSteam
              ObjectOutputStream

    作用:可以读写基本数据类型的变量、引用数据类型的变量。

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

如下两个过程使用的流:
序列化过程:使用0bject0utputStream流实现。将内存中的Java对象保存在文件中或通过网

                      络传输出去

反序列化过程:使用0bjectInputSteam流实现。将文件中的数据或网络传输过来的数据还原为内存中的Java对象

自定义类要想实现序列化机制,需要满足:
   1️⃣自定义类需要实现接口:Serializable   Serializable属于标识接口

   2️⃣要求自定义类声明一个全局常量:static final long serialVersionUID= 42234234L;

        用来唯一的标识当前的类。  

   3️⃣要求自定义类的各个属性也必须是可序列化的。
            对于基本数据类型的属性:默认就是可以序列化的
            对于引用数据类型的属性:要求实现Serializable类

注意点:
   ①如果不声明全局常量serialVersionUID,系统会自动声明生成一个针对于当前类的

       serialVersionUID,如果修改此类的话,会导致serialVersionUID变化,进而导致反序列

       化时,出现InvalidClassException异常。

   ②类中的属性如果声明为transient或static,则不会实现序列化。 

//自定义类
class  Product implements Serializable {
    String name;
    int age;
    static final long serialVersionUID= 42234234L;
    public Product(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return  name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "Product{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
//序列化
public void test2() throws Exception{
    File file=new File("abc.txt");
    ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream(file));
    //写出数据即为序列化的过程
    Product p1=new Product("tom",12);
    oos.writeObject(p1);
    oos.flush();
    oos.close();
}
//反序列化
public void test3() throws  Exception{
    File file2=new File("abc.txt");
    ObjectInputStream ois=new ObjectInputStream(new FileInputStream(file2));
    //读取文件中的对象(或反序列化的过程)
    Product product= (Product)ois.readObject();
    System.out.println(product);
    ois.close();
}

 (8)其他流的使用

标准输入、输出流:
System.in:标准的输入流,默认从键盘输入

System.out:标准的输出流,默认从显示器输出(理解为控制台输出)

System.in的类型是InputStream

System.out的类型是PrintStream,其是OutputStream的子类FilterOutputStream 的子类

重定向:通过System类的setIn,setOut方法对默认设备进行改变。

  • public static void setIn(InputStream in)

  • public static void setOut(PrintStream out)

打印流:

实现将基本数据类型的数据格式转化为字符串输出。

打印流:PrintStreamPrintWriter

提供了一系列重载的print()和println()方法,用于多种数据类型的输出

 19. 网络编程

(1)网络编程三要素与InetAddress类的使用

要想实现网络通信,需要解决的三个问题:

    问题1:如何准确地定位网络上一台或多台主机

    问题2:如何定位主机上的特定的应用

    问题3:找到主机后,如何可靠、高效地进行数据传输

实现网络传输的三个要素:(对应解决三个问题):
    使用IP地址(准确地定位网络上一台或多台主机)
    使用端口号(定位主机上的特定的应用)
    规范网络通信协议(可靠、高效地进行数据传输)

通信要素1:IP地址
   作用: IP地址用来给网络中的一台计算机设备做唯一的编号

   IP地址分类:

       IP地址分类方式1:  IPv4(占用4个字节)  IPv6(占用16个字节)

       IP地址分类方式2: 公网地址(万维网使用)和私有地址(局域网使用,以192.168开头)

   本地回路地址:127.0.0.1

   域名:便捷的记录ip地址
              例如:www.baidu.com      www.atguigu.com      www.bilibili.com

通信要素2:端口号

    可以唯一标识主机中的进程(应用程序)

    不同的进程分配不同的端口号

    范围:用两个字节表示的整数,它的取值范围是0~65535。

通信要素3:通信协议

    网络通信协议的目的: 为了实现可靠而高效的数据传输。

    网络参考模型:

        0SI参考模型:将网络分为7层,过于理想化,没有实施起来。

        TCP/IP参考模型: 将网络分为4层:应用层、传输层、网络层、物理+数据链路层。事实

                                    上使用的标准。

InetAddress的使用:

    作用:InetAddress类的一个实例就代表一个具体的ip地址。

    两个子类:Inet4Address、Inet6Address。

    InetAddress 类没有提供公共的构造器,而是提供了如下几个静态方法来获取

    InetAddress 实例:

       public static InetAddress getLocalHost()

       public static InetAddress getByName(String host)

       public static InetAddress getByAddress(byte[] addr)

    InetAddress 提供了如下几个常用的方法

       public String getHostAddress() :返回 IP 地址字符串(以文本表现形式)

       public String getHostName() :获取此 IP 地址的主机名

       public boolean isReachable(int timeout):测试是否可以达到该地址

        InetAddress a= null;
        InetAddress b=null;
        InetAddress c=null;
        try {
            //获取本地ip对应的InetAddress的实例
            a = InetAddress.getByName("192.168.23.31");  
            b=InetAddress.getByName("www.atguigu.com");
            c=InetAddress.getLocalHost(); //获取指定ip对应的InetAddress的实例
            System.out.println(a); ///192.168.23.31
            System.out.println(b);//www.atguigu.com/121.29.38.197
            System.out.println(c);//LAPTOP-CJNI9ANA/192.168.1.187
            System.out.println(a.getHostName()); //192.168.23.31
            System.out.println(a.getHostAddress());//192.168.23.31
            System.out.println(b.getHostName());//www.atguigu.com
            System.out.println(b.getHostAddress());//221.194.163.251
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

(2)网络编程API

Socket类:

  • 网络上具有唯一标识的IP地址和端口号组合在一起构成唯一能识别的标识符套接字(Socket)。

  • 利用套接字(Socket)开发网络应用程序早已被广泛的采用,以至于成为事实上的标准。网络通信其实就是Socket间的通信。

  • 通信的两端都要有Socket,是两台机器间通信的端点。

  • Socket允许程序把网络连接当成一个流,数据在两个Socket间通过IO传输。

  • 一般主动发起通信的应用程序属客户端,等待通信请求的为服务端。

  • Socket分类:

    • 流套接字(stream socket):使用TCP提供可依赖的字节流服务

      • ServerSocket:此类实现TCP服务器套接字。服务器套接字等待请求通过网络传入。

      • Socket:此类实现客户端套接字(也可以就叫“套接字”)。套接字是两台机器间通信的端点。

    • 数据报套接字(datagram socket):使用UDP提供“尽力而为”的数据报服务

      • DatagramSocket:此类表示用来发送和接收UDP数据报包的套接字。


Socket相关类API:

    ServerSocket类:

          ServerSocket类的构造方法:

                ServerSocket(int port) :创建绑定到特定端口的服务器套接字。

          ServerSocket类的常用方法:

                Socket accept():侦听并接受到此套接字的连接。

    Socket类:

          Socket类的常用构造方法

                public Socket(InetAddress address,int port):创建一个流套接字并将其连接到指定

                                                                                         IP 地址的指定端口号。

                public Socket(String host,int port):创建一个流套接字并将其连接到指定主机上的

                                                                         指定端口号。

          Socket类的常用方法

                public InputStream getInputStream():返回此套接字的输入流,可以用于接收消息

                public OutputStream getOutputStream():返回此套接字的输出流,可以用于发送

                消息

                public InetAddress getInetAddress():此套接字连接到的远程 IP 地址;如果套接

                字是未连接的,则返回 null。

                public InetAddress getLocalAddress():获取套接字绑定的本地地址。

                public int getPort():此套接字连接到的远程端口号;如果尚未连接套接字,则返回

                0。

                public int getLocalPort():返回此套接字绑定到的本地端口。如果尚未绑定套接

                字,则返回 -1。

                public void close():关闭此套接字。套接字被关闭后,便不可在以后的网络连接中

                使用(即无法重新连接或重新绑定)。需要创建新的套接字对象。 关闭此套接字                    也将会关闭该套接字的 InputStream 和 OutputStream。

                public void shutdownInput():如果在套接字上调用 shutdownInput() 后从套接字输

                入流读取内容,则流将返回 EOF(文件结束符)。 即不能在从此套接字的输入流

                中接收任何数据。

                public void shutdownOutput():禁用此套接字的输出流。对于TCP 套接字,任何

                以前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列。 如果在套接字

                上调用

                shutdownOutput() 后写入套接字输出流,则该流将抛出 IOException。 即不能通

                过此套接字的输出流发送任何数据。

注意:先后调用Socket的shutdownInput()和shutdownOutput()方法,仅仅关闭了输入流和输出流,并不等于调用Socket的close()方法。在通信结束后,仍然要调用Scoket的close()方法,因为只有该方法才会释放Socket占用的资源,比如占用的本地端口号等。


DatagramSocket类:

   DatagramSocket 类的常用方法:

  • public DatagramSocket(int port)创建数据报套接字并将其绑定到本地主机上的指定端口。套接字将被绑定到通配符地址,IP 地址由内核来选择。

  • public DatagramSocket(int port,InetAddress laddr)创建数据报套接字,将其绑定到指定的本地地址。本地端口必须在 0 到 65535 之间(包括两者)。如果 IP 地址为 0.0.0.0,套接字将被绑定到通配符地址,IP 地址由内核选择。

  • public void close()关闭此数据报套接字。

  • public void send(DatagramPacket p)从此套接字发送数据报包。DatagramPacket 包含的信息指示:将要发送的数据、其长度、远程主机的 IP 地址和远程主机的端口号。

  • public void receive(DatagramPacket p)从此套接字接收数据报包。当此方法返回时,DatagramPacket 的缓冲区填充了接收的数据。数据报包也包含发送方的 IP 地址和发送方机器上的端口号。 此方法在接收到数据报前一直阻塞。数据报包对象的 length 字段包含所接收信息的长度。如果信息比包的长度长,该信息将被截短。

  • public InetAddress getLocalAddress()获取套接字绑定的本地地址。

  • public int getLocalPort()返回此套接字绑定的本地主机上的端口号。

  • public InetAddress getInetAddress()返回此套接字连接的地址。如果套接字未连接,则返回 null。

  • public int getPort()返回此套接字的端口。如果套接字未连接,则返回 -1。


DatagramPacket类

​​​​​​​   DatagramPacket类的常用方法:
  • public DatagramPacket(byte[] buf,int length)构造 DatagramPacket,用来接收长度为 length 的数据包。 length 参数必须小于等于 buf.length。

  • public DatagramPacket(byte[] buf,int length,InetAddress address,int port)构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。length 参数必须小于等于 buf.length。

  • public InetAddress getAddress()返回某台机器的 IP 地址,此数据报将要发往该机器或者是从该机器接收到的。

  • public int getPort()返回某台远程主机的端口号,此数据报将要发往该主机或者是从该主机接收到的。

  • public byte[] getData()返回数据缓冲区。接收到的或将要发送的数据从缓冲区中的偏移量 offset 处开始,持续 length 长度。

  • public int getLength()返回将要发送或接收到的数据的长度。 ​​​​​​​​​​​​​​​​​​​​​

 (3)TCP与UDP协议剖析 

通信的协议还是比较复杂的,java.net 包中包含的类和接口,它们提供低层次的通信细节。我们可以直接使用这些类和接口,来专注于网络程序开发,而不用考虑通信的细节。

java.net 包中提供了两种常见的网络协议的支持:

  • UDP:用户数据报协议(User Datagram Protocol)。

  •  TCP:传输控制协议 (Transmission Control Protocol)。

TCP协议:

  • TCP协议进行通信的两个应用进程:客户端、服务端。

  • 使用TCP协议前,须先建立TCP连接,形成基于字节流的传输数据通道

  • 传输前,采用“三次握手”方式,点对点通信,是可靠的

    • TCP协议使用重发机制,当一个通信实体发送一个消息给另一个通信实体后,需要收到另一个通信实体确认信息,如果没有收到另一个通信实体确认信息,则会再次重复刚才发送的消息。

  • 在连接中可进行大数据量的传输

  • 传输完毕,需释放已建立的连接,效率低

UDP协议:

  • UDP协议进行通信的两个应用进程:发送端、接收端。

  • 将数据、源、目的封装成数据包(传输的基本单位),不需要建立连接

  • 发送不管对方是否准备好,接收方收到也不确认,不能保证数据的完整性,故是不可靠的

  • 每个数据报的大小限制在64K

  • 发送数据结束时无需释放资源,开销小,通信效率高

  • 适用场景:音频、视频和普通数据的传输。例如视频会议

TCP生活案例:打电话

UDP生活案例:发送短信、发电报


三次握手:

TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。

  • 第一次握手,客户端向服务器端发起TCP连接的请求

  • 第二次握手,服务器端发送针对客户端TCP连接请求的确认

  • 第三次握手,客户端发送确认的确认

四次挥手:


通信模型:

Java语言的基于套接字TCP编程分为服务端编程和客户端编程,其通信模型如图所示:

开发步骤:

客户端程序包含以下四个基本的步骤 :

  • 创建 Socket :根据指定服务端的 IP 地址或端口号构造 Socket 类对象。若服务器端响应,则建立客户端到服务器的通信线路。若连接失败,会出现异常。

  • 打开连接到 Socket 的输入/ 出流: 使用 getInputStream()方法获得输入流,使用getOutputStream()方法获得输出流,进行数据传输

  • 按照一定的协议对 Socket 进行读/ 写操作:通过输入流读取服务器放入线路的信息(但不能读取自己放入线路的信息),通过输出流将信息写入线路。

  • 关闭 Socket :断开客户端到服务器的连接,释放线路

服务器端程序包含以下四个基本的步骤:

  • 调用 ServerSocket(int port) :创建一个服务器端套接字,并绑定到指定端口上。用于监听客户端的请求。

  • 调用 accept() :监听连接请求,如果客户端请求连接,则接受连接,返回通信套接字对象。

  • 调用该Socket 类对象的 getOutputStream() 和 getInputStream () :获取输出流和输入流,开始网络数据的发送和接收。

  • 关闭Socket 对象:客户端访问结束,关闭通信套接字。

(4)UDP、URL网络编程

UDP(User Datagram Protocol,用户数据报协议):是一个无连接的传输层协议、提供面向事务的简单不可靠的信息传送服务,类似于短信。

通信模型:

开发步骤

发送端程序包含以下四个基本的步骤:

  • 创建DatagramSocket :默认使用系统随机分配端口号。

  • 创建DatagramPacket:将要发送的数据用字节数组表示,并指定要发送的数据长度,接收方的IP地址和端口号。

  • 调用 该DatagramSocket 类对象的 send方法 :发送数据报DatagramPacket对象。

  • 关闭DatagramSocket 对象:发送端程序结束,关闭通信套接字。

接收端程序包含以下四个基本的步骤 :

  • 创建DatagramSocket :指定监听的端口号。

  • 创建DatagramPacket:指定接收数据用的字节数组,起到临时数据缓冲区的效果,并指定最大可以接收的数据长度。

  • 调用 该DatagramSocket 类对象的receive方法 :接收数据报DatagramPacket对象。。

  • 关闭DatagramSocket :接收端程序结束,关闭通信套接字。


URL编程:

URL类:

  • URL(Uniform Resource Locator):统一资源定位符,它表示 Internet 上某一资源的地址。

  • 通过 URL 我们可以访问 Internet 上的各种网络资源,比如最常见的 www,ftp 站点。浏览器通过解析给定的 URL 可以在网络上查找相应的文件或其他资源。

  • URL的基本结构由5部分组成:<传输协议>://<主机名>:<端口号>/<文件名>#片段名?参数列表

  • 例如: http://192.168.1.100:8080/helloworld/index.jsp#a?username=shkstart&password=123

    • 片段名:即锚点,例如看小说,直接定位到章节

    • 参数列表格式:参数名=参数值&参数名=参数值....

  • 为了表示URL,java.net 中实现了类 URL。我们可以通过下面的构造器来初始化一个 URL 对象:

    • public URL (String spec):通过一个表示URL地址的字符串可以构造一个URL对象。例如:

      URL url = new URL("http://www. atguigu.com/"); 
    • public URL(URL context, String spec):通过基 URL 和相对 URL 构造一个 URL 对象。例如:

      URL downloadUrl = new URL(url, “download.html")

      public URL(String protocol, String host, String file); 例如:

      URL url = new URL("http", "www.atguigu.com", “download. html");
    • public URL(String protocol, String host, int port, String file); 例如:

      URL gamelan = new URL("http", "www.atguigu.com", 80, “download.html");
  • URL类的构造器都声明抛出非运行时异常,必须要对这一异常进行处理,通常是用 try-catch 语句进行捕获。

URL类常用方法:

一个URL对象生成后,其属性是不能被改变的,但可以通过它给定的方法来获取这些属性:

  • public String getProtocol( ) 获取该URL的协议名

  • public String getHost( ) 获取该URL的主机名

  • public String getPort( ) 获取该URL的端口号

  • public String getPath( ) 获取该URL的文件路径

  • public String getFile( ) 获取该URL的文件名

  • public String getQuery( ) 获取该URL的查询名

URL url = new URL("http://localhost:8080/examples/myTest.txt");
System.out.println("getProtocol() :"+url.getProtocol());
System.out.println("getHost() :"+url.getHost());
System.out.println("getPort() :"+url.getPort());
System.out.println("getPath() :"+url.getPath());
System.out.println("getFile() :"+url.getFile());
System.out.println("getQuery() :"+url.getQuery());

针对HTTP协议的URLConnection类

  • URL的方法 openStream():能从网络上读取数据

  • 若希望输出数据,例如向服务器端的 CGI (公共网关接口-Common Gateway Interface-的简称,是用户浏览器和服务器端的应用程序进行连接的接口)程序发送一些数据,则必须先与URL建立连接,然后才能对其进行读写,此时需要使用 URLConnection 。

  • URLConnection:表示到URL所引用的远程对象的连接。当与一个URL建立连接时,首先要在一个 URL 对象上通过方法 openConnection() 生成对应的 URLConnection 对象。如果连接过程失败,将产生IOException.

  • 通过URLConnection对象获取的输入流和输出流,即可以与现有的CGI程序进行交互。

    • public Object getContent( ) throws IOException

    • public int getContentLength( )

    • public String getContentType( )

    • public long getDate( )

    • public long getLastModified( )

    • public InputStream getInputStream ( ) throws IOException

    • public OutputSteram getOutputStream( )throws IOException

20. 反射机制 

(1)反射、Class的理解与获取Class实例的方式

反射概况:

         Reflection(反射)是被视为动态语言的关键,反射机制允许程序在运行期间借助于

         Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。

         加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个

         Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类

         的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之

   为:反射。 

public class Person {
    private  String name;
    public  int age;
    public Person(){
        System.out.println("Person()...");
    }
    public Person(int age){
        this.age = age;
    }
    private  Person(String name, int age){
        this.name = name;
        this.age = age;
    }
    public void show(){
        System.out.println("我是一个person");
    }
    private  String showNation(String nation){
        return "我的国籍是:"+nation;
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
public class ReflectionTest {
    //使用反射之前可执行操作
    @Test
    public void test(){
        Person p1=new Person();  //创建Person类的实例
        p1.age=10;  //调用属性
        System.out.println(p1.age);
        p1.show();//调用方法
    }
    //使用反射之后可执行操作
    @Test
    public void test2() throws Exception{
        //1.创建Person类的实例
        Class<Person> clazz=Person.class;
        Person p1= clazz.newInstance();
        System.out.println(p1);
        //2.调用属性
        Field ageField= clazz.getField("age");
        ageField.set(p1,10);
        System.out.println(ageField.get(p1));
        //3.调用方法
        Method showMethod=clazz.getMethod("show");
        showMethod.invoke(p1);
    }
    //使用反射之后可执行操作
    @Test
    public void test3() throws Exception{
        //出了Person类之后,就不能直接调用Person类中声明的private权限修饰的结构
        //(属性,方法,构造器)
        //但是,我们可以通过反射的方式,调用Person类中私有的结构
        1.创建Person类的实例
        Class clazz=Person.class;
        //通过Class的实例调用getDeclaredConstructor(Class...args),获取指定参数类型的
        //构造器
        Constructor cons= clazz.getDeclaredConstructor(String.class,int.class);
        cons.setAccessible(true);  //setAccessible(true):确保此构造器是可以访问的
        //通过Constructor实例调用newInstance(0bject...objs),返回一个运行时类的实例
        Person p1= (Person) cons.newInstance("Tom",12);
        System.out.println(p1);
        2.调用私有的属性
        //通过Class实例调用getDeclaredField(String fieldName),
        //获取运行时类指定名的属性
        Field nameField= clazz.getDeclaredField("name");
        //通过Filed类的实例调用get(0bject obj)(获取的操作)或 
        //set(0bject obj,0bject value)(设置的操作)进行操作。
        nameField.setAccessible(true);
        nameField.set(p1,"LiMing");
        System.out.println(nameField.get(p1));
        3.调用私有的方法
        //通过Class的实例调用getDeclaredMethod(String methodName,Class...args),
        //获取指定的方法
        Method showMethod=clazz.getDeclaredMethod("showNation", String.class);
        showMethod.setAccessible(true);//setAccessible(true):确保此方法是可访问的
        //通过Method实例调用invoke(0bject obj,0bject...objs),即为对Method对应的方
        //法的调用。invoke()的返回值即为Method对应的方法的返回值
        //特别的:如果Method对应的方法的返回值类型为void,则invoke()返回值为null
        String info=(String) showMethod.invoke(p1, "CHN");
        System.out.println(info);
    }
}

1. 面向对象中创建对象,调用指定结构(属性、方法)等功能,可以不使用反射,也可以使用反射。
   请问有什么区别?
   不使用反射,我们需要考虑封装性。比如:出了Person类之后,就不能调用Person类中私有的
   结构。
   使用反射,我们可以调用运行时类中任意构造器、属性、方法。包括了私有的属性、方法、构造器

2. 以前创建对象并调用方法的方式,与现在通过反射创建对象并调用方法的方式对比的话,哪种
   用的多?场景是什么?
   从我们作为程序员开发者的角度来讲,我们开发中主要是完成业务代码,对于相关的对象、方法
   的调用都是确定的。所以,我们使用非反射的方式多一些。
   因为反射体现了动态性(可以在运行时动态的获取对象所属的类,动态的调用相关的方法),所
   以我们在设计框架的时候,会大量的使用反射。意味着,如果大家需要学习框架源码,那么就需
   要学习反射
   框架=注解+反射 +设计模式

3。单例模式的饿汉式和懒汉式中,私有化类的构造器了!此时通过反射,可以创建单例模式中类的
   多个对象吗?
   是的!

4. 通过反射,可以调用类中私有的结构,是否与面向对象的封装性有冲突?是不是Java语言设计存
   在Bug?
   不存在bug!
   封装性:体现的是是否建议我们调用内部api的问题。比如,private声明的结构,意味着不建议
          调用
   反射:体现的是我们能否调用的问题。因为类的完整结构都加到了内存中,所有我们就有能力进行
         调用

注意:针对于核心源码的api,内部的私有的结构在jdk17中就不可以通过反射调用了。

Java反射机制提供的功能:

   在运行时判断任意一个对象所属的类

   在运行时构造任意一个类的对象

   在运行时判断任意一个类所具有的成员变量和方法

   在运行时获取泛型信息

   在运行时调用任意一个对象的成员变量和方法

   在运行时处理注解

   生成动态代理

反射相关的主要API

 java.lang.Class:代表一个类

    java.lang.reflect.Method:代表类的方法

    java.lang.reflect.Field:代表类的成员变量

    java.lang.reflect.Constructor:代表类的构造器

反射的优缺点

   优点:提高了Java程序的灵活性和扩展性,降低了耦合性,提高自适应能力

              允许程序创建和控制任何类的对象,无需提前硬编码目标类

   缺点:反射的性能较低

                   反射机制主要应用在对灵活性和扩展性要求很高的系统框架上

            反射会模糊程序内部逻辑,可读性较差


​​​​​​​ Class的理解(如下以Java类的加载为例说明):

    针对于编写好的.java源文件进行编译(使用javac.exe),会生成一个或多个.class字节码

    文件。接着,我们使用java.exe命令对指定的.class文件进行解释运行。这个解释运行的过

    程中,我们需要将.class字节码文件加载(使用类的加载器)加载到内存中(存放在方法

    区)。加载到内存中的.class文件对应的结构即为Class的一个实例。

    比如:加载到内存中的Person类或String类或User类,都作为Class的一个一个的实例

              Class clazz1 = Person.class; //运行时类

              Class clazz2 = String.class;

              Class clazz3 = User.class;

              Class clazz4 = Comparable.class;

    说明:运行时类在内存中会缓存起来,在整个执行期间,只会加载一次。

体会:Class看做是反射的源头

获取Class实例的几种方式(掌握前三种):

//1. 调用运行时类的静态属性:class
Class clazz1=Product.class;
System.out.println(clazz1); //class Product
//2. 调用运行时类的对象的getClass()
Product p1=new Product();
Class clazz2=p1.getClass();
System.out.println(clazz1==clazz2); //true
//3. 调用Class的静态方法forName(String className)
String className="Product"; //全类名
Class clazz3=Class.forName(className);
System.out.println(clazz1==clazz3); //true
//使用类的加载器的方式
Class clazz4=ClassLoader.getSystemClassLoader().loadClass("Product");
System.out.println(clazz1==clazz4); //true

Class的实例都可以指向哪些结构呢?

简言之,所有Java类型!
(1)class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
(2)interface:接口
(3)[]:数组
(4)enum:枚举
(5)annotation:注解@interface
(6)primitive type:基本数据类型
(7)void

举例:
  Class c1 = Object.class;
  Class c2 = Comparable.class;
  Class c3 = String[].class;
  Class c4 = int[][].class;
  Class c5 = ElementType.class;
  Class c6 = Override.class;
  Class c7 = int.class;
  Class c8 = void.class;
  Class c9 = Class.class;

int[] a = new int[10];
int[] b = new int[100];
Class c10 = a.getClass();
Class c11 = b.getClass();
// 只要元素类型与维度一样,就是同一个Class
System.out.println(c10 == c11); //true

(2)类的加载过程与类加载器的理解

类的加载过程:

当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、链接、初始化三个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载。

类的加载又分为三个阶段:

   (1)装载(Loading) 将类的 class 文件读入内存,并为之创建一个 java.lang.Class 对

            象。此过程由类加载器完成

   (2)链接(Linking)

            ①验证 Verify:确保加载的类信息符合 JVM 规范,例如:以cafebabe 开头,没有安

               全方面的问题。

           ②准备 Prepare:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,

               这些内存都将在方法区中进行分配。

           ③解析 Resolve:虚拟机常量池内的符号引用(常量名)替换为直接引用(地 址)的

               过程。

    (3)初始化(Initialization)

           ①执行类构造器()方法的过程。类构造器()方法是由编译期自动收集类中所有类变量的

               赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构

               造该类对象的构造器)。

           ②当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类

               的初始化。

           ③虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步。


类加载器(classloader)

类加载器的作用: 将 class 文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的 java.lang.Class 对象,作为方法区中类数据的访问入口。

类缓存:标准的 JavaSE 类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过 JVM 垃圾回收机制可以回收这些 Class 对象。

类加载器的分类(JDK8 为例):

JVM 支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。

从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是 Java 虚拟机规范却没有这么定义,而是将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器。无论类加载器的类型如何划 分,在程序中我们最常见的类加载器结构主要是如下情况:

(1)启动类加载器(引导类加载器,Bootstrap ClassLoader)

         • 这个类加载使用 C/C++语言实现的,嵌套在 JVM 内部。获取它的对象时往往返回

           null

         • 它用来加载 Java 的核心库(JAVA_HOME/jre/lib/rt.jar 或 sun.boot.class.path 路径下

           的内容)。用于提供 JVM 自身需要的类。

         • 并不继承自 java.lang.ClassLoader,没有父加载器。

         • 出于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类

         • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。

(2)扩展类加载器(Extension ClassLoader)

        • Java 语言编写,由 sun.misc.Launcher$ExtClassLoader 实现。

        • 继承于 ClassLoader 类

        • 父类加载器为启动类加载器

        • 从 java.ext.dirs 系统属性所指定的目录中加载类库,或从 JDK 的安装目录的 jre/lib/ext

          子目录下加载类库,如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载

(3)应用程序类加载器(系统类加载器,AppClassLoader)

        • java 语言编写,由 sun.misc.Launcher$AppClassLoader 实现

        • 继承于 ClassLoader 类

        • 父类加载器为扩展类加载器

        • 它负责加载环境变量 classpath 或系统属性 java.class.path 指定路径下的类库

        • 应用程序中的类加载器默认是系统类加载器。

        • 它是用户自定义类加载器的默认父加载器

        • 通过 ClassLoader 的 getSystemClassLoader()方法可以获取到该类加载器

(4)用户自定义类加载器(了解)

        • 在 Java 的日常应用程序开发中,类的加载几乎是由上述 3 种类加载器相互配合执行

          的。在必要时,我们还可以自定义类加载器,来定制类的加载方式。

        • 体现 Java 语言强大生命力和巨大魅力的关键因素之一便是,Java 开发者可以自定义

           类加载器来实现类库的动态加载,加载源可以是本地的 JAR 包,也可以是网络上的远

           程资源。

        • 同时,自定义加载器能够实现应用隔离,例如 Tomcat,Spring 等中间件和组件框架

          都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块。这种机

          制比 C/C++程序要好太多,想不修改 C/C++程序就能为其新增功能,几乎是不可能

          的,仅仅一个兼容性便能阻挡住所有美好的设想。

        • 自定义类加载器通常需要继承于 ClassLoader

以上的类的加载器是否存在继承关系?No!

21. JDK8-17新特性

(1) 新特性的概述

发布特点(小步快跑,快速迭代):  

名词解释:Oracle JDK和Open JDK

这两个JDK最大不同就是许可证不一样。但是对于个人用户来讲,没区别

Oracle JDKOpen JDK
来源Oracle团队维护Oracle和Open Java社区
授权协议Java 17及更高版本 Oracle Java SE 许可证 Java16及更低版本甲骨文免费条款和条件 (NFTC) 许可协议GPL v2许可证
关系由Open JDK构建,增加了少许内容
是否收费2021年9月起Java17及更高版本所有用户免费。 16及更低版本,个人用户、开发用户免费。2017年9月起,所有版本免费
对语法的支持一致一致

名词解释:JEP:

JEP(JDK Enhancement Proposals):jdk 改进提案,每当需要有新的设想时候,JEP可以提出非正式的规范(specification),被正式认可的JEP正式写进JDK的发展路线图并分配版本号。

名词解释:LTS:

LTS(Long-term Support)即长期支持。Oracle官网提供了对Oracle JDK个别版本的长期支持,即使发发行了新版本,比如目前最新的JDK19,在结束日期前,LTS版本都会被长期支持。(出了bug,会被修复,非LTS则不会再有补丁发布)所以,一定要选一个LTS版本,不然出了漏洞没人修复了。

如果要选择Oracle JDK,目前可选的LTS版本为8、11、17三个。

版本开始日期结束日期延期结束日期
7(LTS)2011年7月2019年7月2022年7月
8(LTS)2014年3月2022年3月2030年12月
11(LTS)2018年9月2023年9月2026年9月
17(LTS)2021年9月2026年9月2029年9月
21(LTS)2023年9月2028年9月2031年9月

各版本支持时间路线图:

​​​​​​​

(2)lambda表达式的使用与函数式接口的理解 

Lambda 是一个匿名函数,我们可以把 Lambda 表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。使用它可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。

语法:

    Lambda 表达式:在Java 8 语言中引入的一种新的语法元素和操作符。这个操作符为 “->

    , 该操作符被称为 Lambda 操作符箭头操作符。它将 Lambda 分为两个部分:

  • 左侧: Lambda形参列表,对应着要重写的接口中的抽象方法的形参列表。

  • 右侧:Lambda体,对应着接口的实现类要重写的方法的方法体。

    Lambda形参列表-> lambda体

举例:

public void test1()  {
        //语法格式一:无参,无返回值
        Runnable  p1=new Runnable(){
             @Override
             public void run() {
                 System.out.println("我爱我家");
             }
         };
         p1.run();
         Runnable p2=()->{
             System.out.println("我爱我家");
         };
         p2.run();
}

public void test2() {
        //语法格式二:Lambda需要一个参数,但是没有返回值。
        Consumer<String> con=new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        };
        con.accept("你好");
        Consumer<String> con1=(String s)->{
            System.out.println(s);
        };
        con1.accept("好");
}

public void test3() {
        //语法格式三:数据类型可以省略,因为可由编译器推断得出,称为”类型推断”
        Consumer<String> con=new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        };
        con.accept("你好");
        Consumer<String> con1=(s)->{
            System.out.println(s);
        };
        con1.accept("好");
}

public void test4() {
        //语法格式四:Lambda若只需要一个参数时,参数的小括号可以省略
        Consumer<String> con=new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        };
        con.accept("你好");
        Consumer<String> con1=s->{
            System.out.println(s);
        };
        con1.accept("好");
}

public void test5() {
        //语法格式五:Lambda需要两个或以上的参数,多条执行语句,并且可以有返回值
        Comparator<Integer> com1=new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                System.out.println(o1);
                System.out.println(o2);
                return o1.compareTo(o2);
            }
        };
        System.out.println(com1.compare(1, 2));
        Comparator<Integer> com2=(o1,o2)->{
            System.out.println(o1);
            System.out.println(o2);
            return o1.compareTo(o2);
        };
        System.out.println(com2.compare(1, 2));
}

public void test6() {
        //语法格式六:当Lambda体只有一条语句时,return与大括号若有,都可以省略
        Comparator<Integer> com1=new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1.compareTo(o2);
            }
        };
        System.out.println(com1.compare(1, 2));
        Comparator<Integer> com2=(o1, o2) ->o1.compareTo(o2);
        System.out.println(com2.compare(1,2));
}

关于类型推断:

在语法格式三 Lambda 表达式中的参数类型都是由编译器推断得出的。Lambda 表达式中
无需指定类型,程序依然可以编译,这是因为 javac 根据程序的上下文,在后台推断出了
参数的类型。Lambda 表达式的类型依赖于上下文环境,是由编译器推断出来的。这就是
所谓的“类型推断”。

举例:
@Test
public void test() {
    //类型推断1
    ArrayList<String> list = new ArrayList<>();
    //类型推断2
    int[] arr = {1, 2, 3};

}

Lambda表达式的本质:

    一方面,lambda表达式作为接口的实现类的对象。

    另一方面,lambda表达式是一个匿名函数。 
什么是函数式接口?为什么需要函数式接口?

   如果接口中只声明有一个抽象方法,则此接口就称为函数式接口。

   因为只有给函数式接口提供实现类的对象时,我们才可以使用Lambda表达式。

四大核心函数式接口:

函数式接口称谓参数类型用途
Consumer<T>消费型接口T对类型为T的对象应用操作,包含方法: void accept(T t)
Supplier<T>供给型接口返回类型为T的对象,包含方法:T get()
Function<T, R>函数型接口T对类型为T的对象应用操作,并返回结果。结果是R类型的对象。包含方法:R apply(T t)
Predicate<T>判断型接口T确定类型为T的对象是否满足某约束,并返回 boolean 值。包含方法:boolean test(T t)

Lambda表达式的语法规则总结:
    ->的左边:Lambda形参列表,参数的类型都可以省略。如果形参只有一个,则一对()

                     也可以省略。

    ->的右边:Lambda体,对应着重写的方法的方法体。如果方法体中只有一行执行语句,

                      则一对{}可以省略。如果有return关键字,则必须一并省略。

(3) 方法引用与构造器引用

举例:Integer::compare;

方法引用的理解: 
    方法引用,可以看做是基于lambda表达式的进一步刻画。

    当需要提供一个函数式接口的实例时,我们可以使用Lambda表达式提供此实例。
    当满足一定的条件的情况下,我们还可以使用方法引用或构造器引用替换Lambda表达

    式。

方法引用的本质:方法引用作为了函数式接口的实例。

具体使用情况说明:

情况1:对象::实例方法
要求:函数式接口中的抽象方法a与其内部实现时调用的对象的某个方法b的形参列表和返回值
      类型都相同(或一致)。此时,可以考虑使用方法b实现对方法a的替换、覆盖。此替换
      或覆盖即为方法引用。
注意:此方法b是非静态的方法,需要对象调用。
//1
Product p1=new Product("tom",12);
Supplier<String> sup1=new Supplier<String>() {
    @Override
    public String get() {
       return p1.getName();
    }
};
System.out.println(p1.getName());
//2. lambda表达式
Supplier<String> sup2=()->p1.getName();
System.out.println(sup2.get());
//3. 方法引用
Supplier<String> sup3=p1::getName;
System.out.println(sup3.get());

情况2:类::静态方法
要求:函数式接口中的抽象方法a与其内部实现时调用的类的某个静态方法b的形参列表和返回值类
     型都相同(或一致)。此时,可以考虑使用方法b实现对方法a的替换、覆盖。此替换或覆盖即
     为方法引用。
注意:此方法b是静态的方法,需要类调用。
 //1
Comparator<Integer> com1=new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
         return  Integer.compare(o1,o2);
    }
};
System.out.println(com1.compare(12,21));
//2. lambda表达式
Comparator<Integer> com2=(o1, o2) ->Integer.compare(o1,o2);
System.out.println(com2.compare(21,43));
//3. 方法引用
Comparator<Integer> com3=Integer::compare;
System.out.println(com3.compare(1,2));

情况3:类::实例方法
要求:函数式接口中的抽象方法a与其内部实现时调用的对象的某个方法b的返回值类型相同。同时,
     抽象方法a中有n个参数,方法b中有n-1个参数,且抽象方法a的第1个参数作为方法b的调用者,
     且抽象方法a的后n-1个参数与方法b的n-1个参数的类型相同(或一致)。则可以考虑使用方法
     b实现对方法a的替换、覆盖。此替换或覆盖。此替换或覆盖即为方法引用。
注意:此方法b是非静态的方法,需要对象调用。但是形式上,写出对象a所属的类
//1
Comparator<String> com1=new Comparator<String>() {
     @Override
     public int compare(String o1,String o2) {
         return  o1.compareTo(o2);
     }
};
System.out.println(com1.compare("aa","as"));
//2. lambda表达式
Comparator<String> com2=(o1, o2) ->o1.compareTo(o2);
System.out.println(com2.compare("dd","df"));
//3. 方法引用
Comparator<String> com3=String::compareTo;
System.out.println(com3.compare("ff","gg"));

构造器引用:

格式:类名::new
说明:
调用了类名对应的类中的某一个确定的构造器
具体调用的是类中的哪一个构造器取决于函数式接口的抽象方法的形参列表!
例如:
Supplier<Exception> sup1=new Supplier<Exception>() {
    @Override
    public Exception get() {
       return  new Exception();
    }
};
System.out.println(sup1.get());
//2. 构造器引用
Supplier<Exception> sup2=Exception::new;
System.out.println(sup2.get());

数组引用
格式:数组名[]::new
例如:
Function<Integer,CallableTest[]>fun1=new Function<Integer, CallableTest[]>() {
   @Override
   public CallableTest[] apply(Integer length) {
        return new CallableTest[length];
   }
};
System.out.println(fun1.apply(10).length);
//2. 构造器引用
Function<Integer, CallableTest[]> fun2=CallableTest[]::new;
System.out.println(fun2.apply(20).length);

(4) StreamAPl使用三环节:实例化、中间操作、终止操作

说明:

    • Java8 中有两大最为重要的改变。第一个是 Lambda 表达式;另外一个则是 Stream

      API。

   • Stream API ( java.util.stream) 把真正的函数式编程风格引入到 Java 中。这是目前为止

     对 Java 类库最好的补充,因为 Stream API 可以极大提供 Java 程序员的生产力,让程序

     员写出高效率、干净、简洁的代码。

   • Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可

     以执行非常复杂的查找、过滤和映射数据等操作。 使用 Stream API 对集合数据进行操

     作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。

     简言之,Stream API 提供了一种高效且易于使用的处理数据的方式。

为什么要使用 Stream API?

实际开发中,项目中多数数据源都来自于 MySQL、Oracle 等。但现在数据源可以更多了,有 MongDB,Radis 等,而这些 NoSQL 的数据就需要 Java 层面去处理。

什么是 Stream?

Stream 是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。 Stream 和 Collection 集合的区别:Collection 是一种静态的内存数据结构, 讲的是数据,而 Stream 是有关计算的,讲的是计算。前者是主要面向内存, 存储在内存中,后者主要是面向 CPU,通过 CPU 实现计算。

注意: ①Stream 自己不会存储元素。

            ②Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。

            ③Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。即一旦

                执行终止操作,就执行中间操作链,并产生结果。

            ④ Stream 一旦执行了终止操作,就不能再调用其它中间操作或终止操作了。

Stream API vs 集合框架
     Stream API关注的是多个数据的计算(排序、查找、过滤、映射、遍历等),面向CPU

     的。集合关注的数据的存储,面向内存的。
     Stream API 之于集合,类似于SQL之于数据表的查询。

Stream 的操作三个步骤:

    1- 创建 Stream 一个数据源(如:集合、数组),获取一个流

    2- 中间操作每次处理都会返回一个持有结果的新 Stream,即中间操作的方法返回值仍然

        是 Stream 类型的对象。因此中间操作可以是个操作链,可对数据源的数据进行 n 次处

        理,但是在终结操作前,并不会真正执行。

    3- 终止操作(终端操作) 终止操作的方法返回值类型就不再是 Stream 了,因此一旦执行终

        止操作,就结束整个 Stream 操作了。一旦执行终止操作,就执行中间操作链,最终产

        生结果并结束 Stream。

Stream的实例化:

方式一:通过集合
  Java8 中的 Collection 接口被扩展,提供了两个获取流的方法:
    default Stream stream() : 返回一个顺序流
    default Stream parallelStream() : 返回一个并行流
  @Test
  public void test01(){
     List<Integer> list = Arrays.asList(1,2,3,4,5);
     //JDK1.8 中,Collection 系列集合增加了方法
     Stream<Integer> stream = list.stream();
  }

方式二:通过数组
  Java8 中的 Arrays 的静态方法 stream() 可以获取数组流:
    static Stream stream(T[] array): 返回一个流
    public static IntStream stream(int[] array)
    public static LongStream stream(long[] array)
    public static DoubleStream stream(double[] array)
  @Test
  public void test02(){
     String[] arr = {"hello","world"};
     Stream<String> stream = Arrays.stream(arr);
  }
  @Test
  public void test03(){
     int[] arr = {1,2,3,4,5};
     IntStream stream = Arrays.stream(arr);
  }

方式三:通过 Stream 的 of()
  可以调用 Stream 类静态方法 of(), 通过显示值创建一个流。它可以接收任意数量的参数。
    public static Stream of(T... values) : 返回一个流
  @Test
  public void test04(){
    Stream<Integer> stream = Stream.of(1,2,3,4,5);
    stream.forEach(System.out::println);
  }

方式四:创建无限流(了解)
  可以使用静态方法 Stream.iterate() 和 Stream.generate(), 创建无限流。
    迭代 public static Stream iterate(final T seed, final UnaryOperator f) 
    生成 public static Stream generate(Supplier s) 
  @Test
  public void test05() {
    // 迭代
    // public static<T> Stream<T> iterate(final T seed, final
    // UnaryOperator<T> f)
    Stream<Integer> stream = Stream.iterate(0, x -> x + 2);
    stream.limit(10).forEach(System.out::println);
    // 生成
    // public static<T> Stream<T> generate(Supplier<T> s)
    Stream<Double> stream1 = Stream.generate(Math::random);
    stream1.limit(10).forEach(System.out::println);
  }

一系列中间操作:

多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值”。

筛选与切片:

方 法描 述
filter(Predicatep)接收 Lambda , 从流中排除某些元素
distinct()筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
limit(long maxSize)截断流,使其元素不超过给定数量
skip(long n)跳过元素,返回一个扔掉了前 n 个元素的流。 若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补

映射:

方法描述
map(Function f)接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
mapToDouble(ToDoubleFunction f)接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream。
mapToInt(ToIntFunction f)接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream。
mapToLong(ToLongFunction f)接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream。
flatMap(Function f)接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流
排序​​​​​​​:  
方法描述
sorted()产生一个新流,其中按自然顺序排序
sorted(Comparator com)产生一个新流,其中按比较器顺序排序
package com.atguigu.stream;
import org.junit.Test;
import java.util.Arrays;
import java.util.stream.Stream;
public class StreamMiddleOperate {
	@Test
    public void test01(){
        //1、创建Stream
        Stream<Integer> stream = Stream.of(1,2,3,4,5,6);
        //2、加工处理
        //过滤:filter(Predicate p)
        //把里面的偶数拿出来
        /*
         * filter(Predicate p)
         * Predicate是函数式接口,抽象方法:boolean test(T t)
         */
        stream = stream.filter(t -> t%2==0);
        //3、终结操作:例如:遍历
        stream.forEach(System.out::println);
    }
    @Test
    public void test02(){
        Stream.of(1,2,3,4,5,6)
                .filter(t -> t%2==0)
                .forEach(System.out::println);
    }
    @Test
    public void test03(){
        Stream.of(1,2,3,4,5,6,2,2,3,3,4,4,5)
                .distinct()
                .forEach(System.out::println);
    }
    @Test
    public void test04(){
        Stream.of(1,2,3,4,5,6,2,2,3,3,4,4,5)
                .limit(3)
                .forEach(System.out::println);
    }
    @Test
    public void test05(){
        Stream.of(1,2,2,3,3,4,4,5,2,3,4,5,6,7)
                .distinct()  //(1,2,3,4,5,6,7)
                .filter(t -> t%2!=0) //(1,3,5,7)
                .limit(3)
                .forEach(System.out::println);
    }
    @Test
    public void test06(){
        Stream.of(1,2,3,4,5,6,2,2,3,3,4,4,5)
                .skip(5)
                .forEach(System.out::println);
    }
    @Test
    public void test07(){
        Stream.of(1,2,3,4,5,6,2,2,3,3,4,4,5)
                .skip(5)
                .distinct()
                .filter(t -> t%3==0)
                .forEach(System.out::println);
    }
    @Test
    public void test08(){
        long count = Stream.of(1,2,3,4,5,6,2,2,3,3,4,4,5)
                .distinct()
                .peek(System.out::println)  
                 //Consumer接口的抽象方法  void accept(T t)
                .count();
        System.out.println("count="+count);
    }
    @Test
    public void test09(){
        //希望能够找出前三个最大值,前三名最大的,不重复
        Stream.of(11,2,39,4,54,6,2,22,3,3,4,54,54)
                .distinct()
                .sorted((t1,t2) -> -Integer.compare(t1, t2))
                //Comparator接口  int compare(T t1, T t2)
                .limit(3)
                .forEach(System.out::println);
    }
    @Test
    public void test10(){
        Stream.of(1,2,3,4,5)
                .map(t -> t+=1)//Function<T,R>接口抽象方法 R apply(T t)
                .forEach(System.out::println);
    }
    @Test
    public void test11(){
        String[] arr = {"hello","world","java"};

        Arrays.stream(arr)
                .map(t->t.toUpperCase())
                .forEach(System.out::println);
    }
    @Test
    public void test12(){
        String[] arr = {"hello","world","java"};
        Arrays.stream(arr)
                .flatMap(t -> Stream.of(t.split("|")))
                //Function<T,R>接口抽象方法 R apply(T t)  现在的R是一个Stream
                .forEach(System.out::println);
    } 
}

终止操作:

  • 终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、Integer,甚至是 void 。 

  • 流进行了终止操作后,不能再次使用。

1-匹配与查找 ​​​​​​​:
方法描述
allMatch(Predicate p)检查是否匹配所有元素
anyMatch(Predicate p) 检查是否至少匹配一个元素
noneMatch(Predicate p)检查是否没有匹配所有元素
findFirst()返回第一个元素
findAny()返回当前流中的任意元素
count()返回流中元素总数
max(Comparator c)返回流中最大值
min(Comparator c)返回流中最小值
forEach(Consumer c)内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。 相反,Stream API 使用内部迭代——它帮你把迭代做了)

2-归约:

方法描述
reduce(T identity, BinaryOperator b)可以将流中元素反复结合起来,得到一个值。返回 T
reduce(BinaryOperator b)可以将流中元素反复结合起来,得到一个值。返回 Optional<T>

备注:map 和 reduce 的连接通常称为 map-reduce 模式,因 Google 用它来进行网络搜索而出名。

3-收集:

方 法描 述
collect(Collector c)将流转换为其他形式。接收一个 Collector接口的实现,<br>用于给Stream中元素做汇总的方法

Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、Set、Map)。

另外, Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例,具体方法与实例如下表:

方法**返回类型作用
toListCollector<T, ?, List<T>>把流中元素收集到List

List<Employee> emps= list.stream().collect(Collectors.toList());

方法返回类型作用
toSetCollector<T, ?, Set<T>>把流中元素收集到Set

Set<Employee> emps= list.stream().collect(Collectors.toSet());

方法返回类型作用
toCollectionCollector<T, ?, C>把流中元素收集到创建的集合

Collection<Employee> emps =list.stream().collect(Collectors.toCollection(ArrayList::new));

方法返回类型作用
countingCollector<T, ?, Long>计算流中元素的个数

long count = list.stream().collect(Collectors.counting());

方法返回类型作用
summingIntCollector<T, ?, Integer>对流中元素的整数属性求和

int total=list.stream().collect(Collectors.summingInt(Employee::getSalary));

方法返回类型作用
averagingIntCollector<T, ?, Double>计算流中元素Integer属性的平均值

double avg = list.stream().collect(Collectors.averagingInt(Employee::getSalary));

方法返回类型作用
summarizingIntCollector<T, ?, IntSummaryStatistics>收集流中Integer属性的统计值。如:平均值

int SummaryStatisticsiss= list.stream().collect(Collectors.summarizingInt(Employee::getSalary));

方法返回类型作用
joiningCollector<CharSequence, ?, String>连接流中每个字符串

String str= list.stream().map(Employee::getName).collect(Collectors.joining());

方法返回类型作用
maxByCollector<T, ?, Optional<T>>根据比较器选择最大值

Optional<Emp>max= list.stream().collect(Collectors.maxBy(comparingInt(Employee::getSalary)));

方法返回类型作用
minByCollector<T, ?, Optional<T>>根据比较器选择最小值
Optional<Emp> min = list.stream().collect(Collectors.minBy(comparingInt(Employee::getSalary)));
方法返回类型作用
reducingCollector<T, ?, Optional<T>>从一个作为累加器的初始值开始,利用BinaryOperator与流中元素逐个结合,从而归约成单个值
int total=list.stream().collect(Collectors.reducing(0, Employee::getSalar, Integer::sum));
方法返回类型作用
collectingAndThenCollector<T,A,RR>包裹另一个收集器,对其结果转换函数
int how= list.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size));​​​​​​​
方法返回类型作用
groupingByCollector<T, ?, Map<K, List<T>>>根据某属性值对流分组,属性为K,结果为V
Map<Emp.Status, List<Emp>> map= list.stream().collect(Collectors.groupingBy(Employee::getStatus));
方法返回类型作用
partitioningByCollector<T, ?, Map<Boolean, List<T>>>根据true或false进行分区

Map<Boolean,List<Emp>> vd = list.stream().collect(Collectors.partitioningBy(Employee::getManage));

(5)JDK8之后的语法新特性汇总及API层面变化 

Java的REPL工具: jShell命令:

Java 终于拥有了像Python 和 Scala 之类语言的REPL工具(交互式编程环境,read - evaluate - print - loop):jShell。以交互式的方式对语句和表达式进行求值。即写即得快速运行

利用jShell在没有创建类的情况下,在命令行里直接声明变量,计算表达式,执行语句。无需跟人解释”public static void main(String[] args)”这句"废话"。

常用指令:
/help:获取有关使用jshell 工具的信息
/help intro:jshell 工具的简介
/list:列出当前session里所有有效的代码片段
/vars:查看当前session 下所有创建过的变量
/methods:查看当前session 下所有创建过的方法
/imports:列出导入的包
/history:键入的内容的历史记录
/edit:使用外部代码编辑器来编写Java 代码
/exit:退出jshell 工具


 异常处理之try-catch资源关闭:

**JDK7的新特性**
在try的后面可以增加一个(),在括号中可以声明流对象并初始化。try中的代码执行完毕,
会自动把流对象释放,就不用写finally了。
格式:
   try(资源对象的声明和初始化){
       业务逻辑代码,可能会产生异常
   }catch(异常类型1 e){
       处理异常代码
   }catch(异常类型2 e){
       处理异常代码
   }
说明:
  1.在try()中声明的资源,无论是否发生异常,无论是否处理异常,都会自动关闭资源对象,不
    用手动关闭了。
  2.这些资源实现类必须实现AutoCloseable或Closeable接口,实现其中的close()方法。
    Closeable是AutoCloseable的子接口。Java7几乎把所有的“资源类”(包括文件IO的各种类、
    JDBC编程的Connection、Statement等接口…)都进行了改写,改写后资源类都实现了
    AutoCloseable或Closeable接口,并实现了close()方法。
  3.写到try()中的资源类的变量默认是final声明的,不能修改。
举例:
    try (
        FileWriter fw = new FileWriter("d:/1.txt");
        BufferedWriter bw = new BufferedWriter(fw);
    ) {
        bw.write("hello");
    } catch (IOException e) {
        e.printStackTrace();
    }
举例2:
    //从d:/1.txt(utf-8)文件中,读取内容,写到项目根目录下1.txt(gbk)文件中
    try (
        FileInputStream fis = new FileInputStream("d:/1.txt");
        InputStreamReader isr = new InputStreamReader(fis, "utf-8");
        BufferedReader br = new BufferedReader(isr);

        FileOutputStream fos = new FileOutputStream("1.txt");
        OutputStreamWriter osw = new OutputStreamWriter(fos, "gbk");
        BufferedWriter bw = new BufferedWriter(osw);
    ) {
        String str;
        while ((str = br.readLine()) != null) {
            bw.write(str);
            bw.newLine();
        }
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }

**JDK9的新特性**
try的前面可以定义流对象,try后面的()中可以直接引用流对象的名称。在try代码执行完毕
后,流对象也可以释放掉,也不用写finally了。
格式:
   A a = new A();
   B b = new B();
   try(a;b){
       可能产生的异常代码
   }catch(异常类名 变量名){
       异常处理的逻辑
   }
举例:
    InputStreamReader reader = new InputStreamReader(System.in);
    OutputStreamWriter writer = new OutputStreamWriter(System.out);
    try (reader; writer) {
        //reader是final的,不可再被赋值
        //   reader = null;
    } catch (IOException e) {
        e.printStackTrace();
    }

 局部变量类型推断:

//1.局部变量的实例化
var list = new ArrayList<String>();
var set = new LinkedHashSet<Integer>();

//2.增强for循环中的索引
for (var v : list) {
    System.out.println(v);
}

//3.传统for循环中
for (var i = 0; i < 100; i++) {
    System.out.println(i);
}

//4. 返回值类型含复杂泛型结构
var iterator = set.iterator();
//Iterator<Map.Entry<Integer, Student>> iterator = set.iterator();

不适用场景:
- 声明一个成员变量
- 声明一个数组变量,并为数组静态初始化(省略new的情况下)
- 方法的返回值类型
- 方法的参数类型
- 没有初始化的方法内的局部变量声明
- 作为catch块中异常类型
- Lambda表达式中函数式接口的类型
- 方法引用中函数式接口的类型

注意:
- var不是一个关键字,而是一个类型名,将它作为变量的类型。不能使用var作为类名。
- 这不是JavaScript。var并不会改变Java是一门静态类型语言的事实。编译器负责推断出类型,
  并把结果写入字节码文件,就好像是开发人员自己敲入类型一样。

instanceof的模式匹配:

Java 14之前旧写法:
  if(obj instanceof String){
      String str = (String)obj; //需要强转
      .. str.contains(..)..
  }else{
      ...
  }

Java 14新特性写法:
  if(obj instanceof String str){
      .. str.contains(..)..
  }else{
      ...
  }

举例:
    String str = "abc";
    public void test(Object obj){
        if(obj instanceof String str){//此时的str的作用域仅限于if结构内。
            System.out.println(str.toUpperCase());
        }else{
            System.out.println(str.toLowerCase());
        }
    }

switch表达式:

传统switch声明语句的弊端:
  匹配是自上而下的,如果忘记写break,后面的case语句不论匹配与否都会执行; --->case穿透
  所有的case语句共用一个块范围,在不同的case语句定义的变量名不能重复;
  不能在一个case里写多个执行结果一致的条件;
  整个switch不能作为表达式返回值;

JDK12中预览特性:
  Java 12将会对switch声明语句进行扩展,使用`case L ->`来替代以前的`break;`,省去了
  break 语句,避免了因少写 break 而出错。
  同时将多个 case 合并到一行,显得简洁、清晰,也更加优雅的表达逻辑分支。
  为了保持兼容性,case 条件语句中依然可以使用字符` :` ,但是同一个 switch 结构里不能混用
  `->` 和`:` ,否则编译错误。
  还可以使用变量接收switch表达式的结果。
例如:
        Fruit fruit = Fruit.GRAPE;
        switch(fruit){
            case PEAR -> System.out.println(4);
            case APPLE,MANGO,GRAPE -> System.out.println(5);
            case ORANGE,PAPAYA -> System.out.println(6);
            default -> throw new IllegalStateException("No Such Fruit:" + fruit);
        };
更进一步:
     Fruit fruit = Fruit.GRAPE;
        int numberOfLetters = switch(fruit){
            case PEAR -> 4;
            case APPLE,MANGO,GRAPE -> 5;
            case ORANGE,PAPAYA -> 6;
            default -> throw new IllegalStateException("No Such Fruit:" + fruit);
        };
      System.out.println(numberOfLetters);//5

JDK13中二次预览特性:
  JDK13中引入了yield语句,用于返回值。这意味着,switch表达式(返回值)应该使用yield,
  switch语句(不返回值)应该使用break。
  yield和return的区别在于:return会直接跳出当前循环或者方法,而yield只会跳出当前
  switch块。
例如:
    String x = "3";
    int i = switch (x) {
        case "1" -> 1;
        case "2" -> 2;
        default -> {
            yield 3;
        }
    };
    System.out.println(i);
或者:
    String x = "3";
    int i = switch (x) {
        case "1":
            yield 1;
        case "2":
            yield 2;
        default:
            yield 3;
    };
    System.out.println(i);

JDK17的预览特性:switch的模式匹配:
旧写法:
    String formatted = "unknown";
    if (o instanceof Integer i) {
        formatted = String.format("int %d", i);
    } else if (o instanceof Long l) {
        formatted = String.format("long %d", l);
    } else if (o instanceof Double d) {
        formatted = String.format("double %f", d);
    } else if (o instanceof String s) {
        formatted = String.format("String %s", s);
    }
    return formatted;
模式匹配新写法:
    return switch (o) {
        case Integer i -> String.format("int %d", i);
        case Long l    -> String.format("long %d", l);
        case Double d  -> String.format("double %f", d);
        case String s  -> String.format("String %s", s);
        default        -> o.toString();
    };
直接在 switch 上支持 Object 类型,这就等于同时支持多种类型,使用模式匹配得到具体类型,
大大简化了语法量

文本块:

现实问题:
  在Java中,通常需要使用String类型表达HTML,XML,SQL或JSON等格式的字符串,在进行字符串
  赋值时需要进行转义和连接操作,然后才能编译该代码,这种表达方式难以阅读并且难以维护。
JDK13的新特性:
  使用"""作为文本块的开始符和结束符,在其中就可以放置多行的字符串,不需要进行任何转义。
  因此,文本块将提高Java程序的可读性和可写性。

基本使用:
1. 
  """
  line1
  line2   相当于:"line1\nline2\nline3\n"
  line3
  """
2. 
  "line1\n" +
  "line2\n" +
  "line3\n"
  如果字符串末尾不需要行终止符,则结束分隔符可以放在最后一行内容上。
   """
   line1
   line2             相当于 "line1\nline2\nline3"
   line3"""
3. 文本块可以表示空字符串,但不建议这样做,因为它需要两行源代码:
   String empty = """
   """;

普通文本:
  String text1 = "The Sound of silence\n" +
                "Hello darkness, my old friend\n" +
                "I've come to talk with you again\n" +
                "Because a vision softly creeping\n" +
                "Left its seeds while I was sleeping\n" +
                "And the vision that was planted in my brain\n" +
                "Still remains\n" +
                "Within the sound of silence";
  System.out.println(text1);
使用新特性:
  String text2 = """
                The Sound of silence
                Hello darkness, my old friend
                I've come to talk with you again
                Because a vision softly creeping
                Left its seeds while I was sleeping
                And the vision that was planted in my brain
                Still remains
                Within the sound of silence
                """;
  System.out.println(text2);

HTML语句:
  <html>
    <body>
         <p>Hello, 尚硅谷</p>
    </body>
  </html>
将其复制到Java的字符串中,会展示成以下内容:
  "<html>\n" +
  "    <body>\n" +
  "        <p>Hello, 尚硅谷</p>\n" +
  "    </body>\n" +
  "</html>\n";
即被自动进行了转义,这样的字符串看起来不是很直观,在JDK 13中:
  """
  <html>
    <body>
        <p>Hello, world</p>
    </body>
  </html>
  """;

SQL语句:
  select employee_id,last_name,salary,department_id
  from employees
  where department_id in (40,50,60)
  order by department_id asc
原有方式:
  String sql = "SELECT id,NAME,email\n" +
                "FROM customers\n" +
                "WHERE id > 4\n" +
                "ORDER BY email DESC";
使用新特性:
  String sql1 = """
                SELECT id,NAME,email
                FROM customers
                WHERE id > 4
                ORDER BY email DESC
                """;

JSON字符串:
原有方式:
  String myJson = "{\n" +
                "    \"name\":\"Song Hongkang\",\n" +
                "     \"address\":\"www.atguigu.com\",\n" +
                "    \"email\":\"shkstart@126.com\"\n" +
                "}";
  System.out.println(myJson);
使用新特性:
  String myJson1 = """
                {
                    "name":"Song Hongkang",
                     "address":"www.atguigu.com",
                    "email":"shkstart@126.com"
                }""";
  System.out.println(myJson1);

Record: 

record 是一种全新的类型,它本质上是一个 final 类,同时所有的属性都是 final 修饰,
它会自动编译出 public get 、hashcode 、equals、toString、构造器等结构,减少了代
码编写量。

具体来说:当你用`record` 声明一个类时,该类将自动拥有以下功能:
- 获取成员变量的简单方法,比如例题中的 name() 和 partner() 。注意区别于我们平常getter()
  的写法。
- 一个 equals 方法的实现,执行比较时会比较该类的所有成员属性。
- 重写 hashCode() 方法。
- 一个可以打印该类所有成员属性的 toString() 方法。
- 只有一个构造方法
此外:
- 还可以在record声明的类中定义静态字段、静态方法、构造器或实例方法。
- 不能在record声明的类中定义实例字段;类不能声明为abstract;不能声明显式的父类等。

举例1(旧写法):
class Point {
    private final int x;
    private final int y;
    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    int x() {
        return x;
    }
    int y() {
        return y;
    }
    public boolean equals(Object o) {
        if (!(o instanceof Point)) return false;
        Point other = (Point) o;
        return other.x == x && other.y == y;
    }
    public int hashCode() {
        return Objects.hash(x, y);
    }
    @Override
    public String toString() {
        return "Point{" +
                "x=" + x +
                ", y=" + y +
                '}';
    }
}
举例1(新写法):
record Point(int x, int y) { }

密封类: 

通过密封的类和接口来限制超类的使用,密封的类和接口限制其它可能继承或实现它们的其它类
或接口
具体使用:
- 使用修饰符`sealed`,可以将一个类声明为密封类。密封的类使用保留关键字`permits`列
  出可以直接扩展(即extends)它的类。
- `sealed` 修饰的类的机制具有传递性,它的子类必须使用指定的关键字进行修饰,且只能
   是 `final`、`sealed`、`non-sealed` 三者之一。

举例:
package com.atguigu.java;
public abstract sealed class Shape permits Circle, Rectangle, Square {...}
public final class Circle extends Shape {...} //final表示Circle不能再被继承了
public sealed class Rectangle extends Shape permits TransparentRectangle, 
FilledRectangle {...}
public final class TransparentRectangle extends Rectangle {...}
public final class FilledRectangle extends Rectangle {...}
public non-sealed class Square extends Shape {...} //non-sealed表示可以允许任何类继承

Optional类:

Optional<T>` 类(java.util.Optional) 是一个容器类,它可以保存类型T的值,代表这个值
存在。或者仅仅保存null,表示这个值不存在。如果值存在,则isPresent()方法会返回true,
调用get()方法会返回该对象。
Optional提供很多有用的方法,这样我们就不用显式进行空值检测。

例如:
String star=null;
//使用Optional避免空指针的问题
// ofNullable用来创建一个0ptional实例,value可能是空,也可能非空
Optional<String> optional=Optional.ofNullable(star);
String otherStar="杨幂";
//orElse()如果Optional实例内部的value属性不为null,则返回value.如果value为null,
//返回otherStar
String finalStar=optional.orElse(otherStar);
System.out.println(finalStar.toString());  //杨幂

​​​​​​​

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端小马

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值