JavaSE—Java基础知识

0、数据类型

运算符、操作符等,在操作时基本都会做隐式类型转换

0.1、八大基本类型、三大引用类型

数据类型字节数默认值取值范围
基本数据类型整数类型byte(字节)1(8位)0-2^8**~2^8-1<br>-128~127**
short(短整型)2(16位)0-2^16~2^16-1
int(整型)4(32位)0-2^32**~2^32-1**
long(长整型)8(64位)0L-2^64~2^64-1
浮点类型float(单精度浮点数)4(32位)0.0f
double(双精度浮点数)8(64位)0.0d
字符类型char(字符型)2'\u0000'
布尔类型布尔型(boolean)1,但**根据编译环境而定**false
引用类型数组类型null
接口类型
类类型String、自定义的类
  • 整数默认是int类型

  • 带小数的数默认是double类型

  • float的精度:7位有效数字

  • double的精度:16位有效数字

0.2、八大基本类型的包装类

将基本类型包装成对象,使得其可以调用属性和方法

基本类型包装类型
bytejava.lang.Byte
shortjava.lang.Short
intjava.lang.Integer
longjava.lang.Long
floatjava.lang.Float
doublejava.lang.Double
charjava.lang.Character
booleanjava.lang.Boolean
  • int类型的数值给Integer,自动装箱

  • Integer类型的数值给int,自动拆箱

0.3、(特别重要)基本类型和引用类型的赋值区别

  • 基本类型保存的是值,所有基本类型赋值(=)是按值传递(拷贝赋值),其实也就是常量池引用。

  • 引用类型保存的是对象的地址,引用类型赋值(=)是按引用传递

0.3.1、基本类型和引用类型的核心区别

可否指向对象

0.4、类型转换

  • 子类转父类,即变大,可以隐式转换

  • 父类转子类,即变小,需要显示转换

  • 小数进行计算,可能会产生精度缺失,例如float f = 10.5;

  • 并且Interger会将byte数值范围的数进行缓存,所以Integer a =127;Integer b = 127;这两个对象 == 为true

  • String有字符串缓存池,+号无变量拼接可使用字符串缓存池

  • int类型转换成byte数组,因为int类型是4字节,即32位,所以int转byte可转成4/1(32/8)=4个byte

  • 所以int的256=00000000000000000000000100000000 每8位变成一个byte,就变成了 0010

byte b = 1 + 1; //通过编译,①是固定值 ②没有超过byte的取值范围
byte a = 1;
byte b = a + 1; //编译报错,因为a是变量,可能a会变得很大,超过byte的取值范围

0.4.1、隐式转换(implicit)

将精度小的值赋值给精度大的,会发生隐式转换,

不会发生错误,系统会统一成精度大的

0.4.2、显式转换(强制转换)(explicit)

将精度大的值赋值给精度小的,会报错,

你需要进行强制转换,例如

double d = 10;
int i = (int)d;

把高精度的变成低精度的时候,若没有超过范围,那就正常,超出范围就进行补码计算,

我暂时还不知道改怎么算,就知道127、127.9强转成byte是127,

128、128.9强转成byte是-128,129、129.9强转成byte是-127

0.5、+=操作符

short s=1;
s=s+2;
short x=1;
x+=1;

s=s+2;是先执行 s+2,因为2是int类型,所以会进行隐式转换,将s+2的结果转换成int类型,得到结果后将int类型的结果赋值给short类型的s,报错 x+=1;这句话相当于x=(short)(x+1);因为+=操作符能够将结果强制转换成操作符左边的数据类型,所以不会报错

0.6、b=a++、b=++a

0.6.1、b=a++

先赋值,再让a加1,执行完语句后,a=2,b=1

0.6.2、b=++a

先让a加1,再赋值,执行完语句后,a=2,b=2

0.7、运算符

当移位大于最大位数后,就会mod,

即将int移位34位时,因为int最高32位,所以只会移位2位

0.7.1、&(二进制补码的 与运算)(也可做逻辑与)

5 & 9 相当于 0101 & 1001 = 0001 = 1

0.7.2、|(二进制补码的 或运算)(也可做逻辑或)

5 | 9 相当于 0101 | 1001 = 1101 = 13

0.7.3、^(二进制补码的 异或运算)

5 ^ 9 相当于 0101 ^ 1001 = 1100 = 12

0.7.4、~(二进制补码的 取反运算)

~-5 相当于 ~1 (27个0)0101 变补码= ~1 (27个1)1011 取反= 0 (27个0)0100 变原码=0 (27个0)0100= 4

0.7.5、<<(二进制补码的 左移(带符号左移)运算)

-5<<2 相当于 1 (27个0)0101<<2 变补码= 1 (27个1)1011<<2
=11 1 (25个1)101100 超出范围 截取= 1 (25个1)101100 
变原码= 1 (25个0)010100= -20

0.7.6、>>(二进制补码的 算术右移(带符号右移)运算)

-5>>2 相当于 1 (27个0)0101>>2 变补码= 1 (27个1)1011>>2=11 1 (25个1)10(11) 截取= 1 (27个1)10 变原码= 1 (27个0)10= -2

0.7.7、>>>(二进制补码的 逻辑右移(不带符号右移)运算)

0.7.8、&&(逻辑与)

&& 具有短路功能,即若有多个判断条件,只要有一个为false,就能决定结果为false,那么就不会执行后面的语句了,但是 & 没有短路功能

0.7.9、||(逻辑或)

|| 具有短路功能,即若有多个判断条件,只要有一个为true,就能决定结果为true,那么就不会执行后面的语句了,但是 | 没有短路功能

0.7.10、!(逻辑取反)

!true 就是 false   !false 就是 true

0.7.11、+、-、*、/、%(加减乘除、取余)

0.7.12、三目运算符

int k = a > b?0:1; //a和b比较,为true则赋值左边的,为false则赋值右边的

1、细碎知识

1.1、命名规则

  • 标识符可由 字母、数字、_ 、$ 组成

  • 标识符开头不可为数字

  • 标识符大小写敏感

  • 标识符不可使用Java中的关键字或保留字

  • 标识符长度没有限制

1.1.1、项目名

全小写

1.1.2、包名

全小写

一般都是公司域名的倒写,

例如:www.chwwww. 变成包名就是 com.chw

1.1.3、类、接口

各单词的首字母大写

有包后,类名变成了权限类名,即:包名+类名

1.1.4、方法、变量

第一个单词的首字母小写,后续的各单词的首字母大写

1.1.5、常量

全部大写,各个单词之间用 _ (下划线)分割

1.2、计算规则

所有的计算都是先将原码变成补码,然后使用补码进行计算,

1.3、转义字符

转义字符效果
\n换行符,将光标到下行的开头
\r回车符,将光标移动到行首
\t垂直制表符,将光标移动到下个制表符位置
\\
''
""

1.4、进制

注意:输入的时候你不需要输入符号位,符号位只是负号,或不加

例如byte是八位,但是你只能输入0b1111111(127),

或者-0b10000000(-128)

如果你非要写超出范围的数值,那就做强制转换,但是对超出范围的数值做强制转换的结果一定是 -1

1.4.1、二进制

byte b1 = 0b1001; //是数字零,不是英文字母o

1.4.2、八进制

byte b2 = 0122; //是数字零,不是英文字母o

1.4.3、十进制

byte b3 = 10;

1.4.4、十六进制

byte b4 = 0x61; //是数字零,不是英文字母o

1.5、父类引用指向子类对象

声明的是父类,实际指向的是子类的对象

Animal a = new Cat();

多态、动态链接、向上转型

1.6、精确计算——BigDecimal类

因为普通计算的时候 1.1 中的 . 也会参与运算,所以普通的计算会产生精度缺失

BigDecimal a = BigDecimal.valueOf(2); //赋值
BigDecimal b = BigDecimal.valueOf(1.1); //赋值
​
BigDecimal c = a.subtract(b); //计算,并将结果赋值给c

1.7、main函数的固定格式

public static void main(String[] args){
}
  • main是程序主入口,main(String args[])这样也是可以的

  • public和static可以换位置

  • []可以放到args后面

  • []可以变成...

1.8、一个.java文件中的类限制

  • 一个.java文件即一个class,可有多个类,但是只可以有一个public的类,并且public的类的类名必须与文件名一致

  • 并且可以一个public都没有

  • 并且只能有默认或者public的访问修饰符

1.9、JVM如何找到要执行的class文件?

通过CLASSPATH环境变量配置的路径来找到的

1.10、编码格式

1.10.1、ASCII

ASCII--Amecian Standard Code for Information Interchange,美国信息交换标准代码。主用于表达现代英语和其他西欧语言中的字符。它是现今最通用的单字节编码系统,它只用一个字节的7位,一共表示128个字符。

1.10.2、ISO-8859-1

又称为Latin-1, 是国际标准化组织(ISO)为西欧语言中的字符制定的编码,用一个字节(8位)来为字符编码,与ASCII字符编码兼容。所谓兼容,是指对于相同的字符,它的ASCII字符编码和ISO-8859-1字符编码相同

1.10.3、GB2312

包括对简体中文字符的编码,一共收录了7445个字符(6763个汉字+682个其他字符),它与ASCII字符编码兼容。

1.10.4、GBK

对GB2312字符编码的扩展,收录了21886个字符(21003个字符+其它字符), 它与GB2312字符编码兼容。

1.10.5、Unicode

由国际Unicode协会编制,收录了全世界所有语言文字中的字符,是一种跨平台的字符编码。

Unicode具有两种编码方案:

  1. 用2个字节(16位)编码,被称为UCS-2, Java语言采用;

  2. 用4个字节(32位)编码,被称为UCS-4; UCS(Universal Character Set)是指采用Unicode字符编码的通用字符集

1.10.6、UTF

有些操作系统不完全支持16位或32位的Unicode编码,

UTF(UCS Transformation Format)字符编码能够把Unicode编码转换为操作系统支持的编码,常见的UTF字符编码包括UTF-8、UTF-16、UTF-32

1.10.6.1、UTF-8

使用一至四个字节为每个字符编码,其中大部分汉字采用三个字节编码,少量不常用汉字采用四个字节编码。

因为 UTF-8 是可变长度的编码方式,相对于 Unicode 编码可以减少存储占用的空间,所以被广泛使用

1.10.6.2、UTF-16

使用二或四个字节为每个字符编码,其中大部分汉字采用两个字节编码,少量不常用汉字采用四个字节编码

1.10.6.3、UTF-32

使用四个字节为每个字符编码,使得 UTF-32 占用空间通常会是其它编码的二到四倍。

1.11、Scanner

1.11.1、获取Scanner对象

Scanner sc=new Scanner(System.in);

1.11.2、判断下个输入是否为指定类型

if(sc.hasNextInt()){ 方法体 }
if(sc.hasNext()){ 方法体 }

1.11.3、获取下个指定输入

int i = sc.nextInt();
String s = sc.next();

1.12、类型的小方法 1.12.1、String

1.12.1.1、大小写转换方法

String s1="haha".toUpperCase(); //String全变成大写
String s2="HaHa".toLowerCase(); //String全变成小写

1.12.1.2、截取子串

String s2=s.substring(2, 4); //包含2,不包含4

1.12.1.3、String 与 int 的相互转换

String s1 = Integer.toString(10); //int变String
String s2 = String.valueOf(10); //int变String
String s3 = 10 + ""; //int变String
​
int i1 = Integer.parseInt(s); //String变int
int i2 = Integer.valueOf(s); //String变int

1.13、是否需要导包

只有java.lang、同包下的类不需要导包,其他都需要导包

java.lang.reflect是要导包的,因为只有java.lang不需要导包

1.14、值的计算时机

记住:使用static、final修饰的变量参与计算的时候,在编译期间就能计算出来了,不需要运行就会计算出来。

编译期间译期间就是程序员使用工具编写代码的时候,还没有将代码运行

2、认知

2.1、创始人

詹姆斯·高斯林

2.2、历史版本

  • 1996年1月23日,JDK 1.0发布,Java语言有了第一个正式版本的运行环境。

  • 1998年12月4日,JDK迎来了一个里程碑式的版本JDK 1.2,工程代号为Playground(竞技场), Sun在这个版本中把Java技术体 系拆分为3个方向,分别是:

面向桌面应用开发的J2SE(Java 2 Platform, Standard Edition)

面向企业级开发的J2EE(Java 2 Platform, Enterprise Edition)

面向手机等移动终端开发的J2ME(Java 2 Platform, Micro Edition)

  • 2004年9月30日, JDK 1.5 发布,工程代号Tiger(老虎)

  • 2009年2月19日,工程代号为Dolphin(海豚)的JDK 1.7完成了其第一个里程碑版本

  • 2014年3月18日,Oracle公司发布 Java SE 1.8

2.3、相关名词

2.3.1、SDK

软件开发包,主要包含函数库或者工具等

2.3.2、JDK

Java程序开发工具包,面向java程序的开发者,包括Java工具(javac、java、jdb等)和Java基础的类库(Java API)

JDK包含Java类库:java.io.、java.net.、java.util.* 等

JDK自带JRE,也可以只安装特定JRE,但一般都是只安装JDK

2.3.3、JRE

Java程序运行环境,面向Java程序的使用者,即Java平台,所有程序需要在JRE下才可运行,包括JVM、Java核心类库、支持文件

2.3.4、API

应用程序编程接口

2.3.5、API Document

API说明文档,描述API中的类、方法等使用的方式

2.3.6、JVM(Java Virtual Machine)

  • 是Java中最核心的,在计算机的内存中,虚拟并提供Java代码可以运行的基础环境,完成了跨平台功能,JVM是JRE的一部分

  • 我们编写的Java代码,编译成class文件,加载到JVM后可运行

  • JVM是Java代码和计算机之间的桥梁

2.3.7、CLASSPATH

  • 安装JDK后需要配置的三个环境变量之一

  • 作用是指定类搜索路径,要使用已经编写好的类,JVM就是通过CLASSPATH来寻找类的。是为了程序能找到相应的".class"文件

  • 与应用类加载器有关,应用类加载器会通过CLASSPATH中配置的路径,来查找当前需要执行的java代码所存在的class文件

2.3.8、PATH

  • 安装JDK后需要配置的三个环境变量之一

  • 作用是指定命令搜索路径,在命令行下面执行命令,如javac编译java程序时,它会先到PATH变量所指定的路径中查找看是否能找到相应的命令程序。使他指向JDK的bin目录,这样在控制台下面编译、执行程序时就不需要再键入一大串路径了

2.3.9、JAVA_HOME

  • 安装JDK后需要配置的三个环境变量之一

  • 它指向jdk的安装目录,Eclipse等软件就是通过搜索JAVA_HOME变量来找到并使用安装好的jdk。

  • 指向的是JDK的安装路径,为了方便引用,

  • 归一原则, 当你JDK路径被迫改变的时候, 你仅需更改JAVA_HOME的变量值即可,,否则,你就要更改任何用绝对路径引用jdk目录的东西了

2.4、Java优点

  • 更纯粹的面向对象编程,加速开发的过程。

  • 一次编写/编译,到处运行,代码可以跨平台(前提是装有JDK)

  • 多线程的支持

  • 代码没有指针管理、内存管理,编程人员可以专注于系统的业务功能的开发

  • 字节码的验证机制,保证代码的安全性

  • 开源及强大的生态环境,社区活跃,第三方类库选择丰富

2.5、Java特点

①提供一个解释型环境:加速开发效率、一次编译,到处运行、多线程、支持动态更新

②提供比较简单的语言:

取消指针:程序级别取消,底层还存在,但是不需要程序员去关心

没有内存管理:程序员手动开辟内存(new)、不需要关心内存的回收

2.6、常用命令

2.6.1、javac

编译命令,编译.java文件,生成.class文件

javac -d ./test1 ./test2/Hello.java  //-d可以生成包结构

2.6.1、java

运行命令,运行.class文件,编译生成字节码

verbose参数可以将JVM启动运行时候加载的信息输出,由于内容多,所以输出转向

java -verbose Hello >> a.txt

2.6.1、javadoc

生成API文档命令

2.6.1、javap

反解析命令,可以解析class字节码文件的内容

2.6.1、jar

打包命令

2.6.1.1、使jar包可以直接运行

打jar包的时候就要这样做,让META-INF里面的 .MF文件,在里面添加

Main-Class: class入口

jar -cvfe com/chw/day01/Hello.jar com/chw/day01/Hello com/chw/day01/Hello.class

第一个是你要打成的jar包,第二个是指定的class入口,第三个是你要打包的class

2.6.1.2、jar包的使用过程
  1. System这个类所在的.java文件已经编写完成,并且已经编译成.class文件,System.java文件在src.zip中,编译好的System.class文件在rt.jar中

  2. 这个.class文件所在位置是JVM可以自动加载的指定路径,这样就可以保证JVM把这个.class文件中的字节码(也就是System这个类的代码),加载到JVM内存中

  3. 在Hello中,import导入要用的System类,若这个类在java.lang包中或者在同一个包结构中,那么就不需要导入

3、编程思想

3.1、编程思想

3.1.1、面向过程的编程语言(POP )

以过程为中心的编程语言,所有过程都自己完成,

例如:你想吃一个汉堡

那么你要自己买材料,自己做汉堡,做完汉堡然后吃汉堡

面向过程是一种以事件为中心的编程思想,编程的时候把解决问题的步骤分析出来,然后用函数把这些步骤实现,在一步一步的具体步骤中再按顺序调用函数。

3.1.2、面向对象的编程语言(OOP object oriented programming)

将现实世界的事物抽象成对象,

例如:你想吃一个汉堡

那么你只需要点餐点一个汉堡,拿到后吃汉堡即可

面向对象是一种以“对象”为中心的编程思想,把要解决的问题分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个对象在整个解决问题的步骤中的属性和行为。

3.1.3、函数式编程(FP functional programming)

是面向计算机硬件的抽象,有变量、赋值语句、表达式、控制语句等,可以理解为 命令式编程就是冯诺伊曼的指令序列。 它的主要思想是关注计算机执行的步骤,即一步一步告诉计算机先做什么再做什么。

3.2、各语言的编程思想

3.2.1、C

面向过程的编程语言

3.2.2、C++

半面向过程半面向对象的编程语言,在面向过程的C语言基础上增加了面向对象的编程

3.2.3、Java

面向对象的编程语言

Java是接近于人的自然思维方式

  • 类:将具有相同属性和行为抽取的模板

  • 对象:类的具体化,通过new操作 #

4、四大引用类型

4.1、强引用

  • Java的new就是强引用

A a = new A();
  • 创建了A对象,并将这个对象的强引用存到变量a中,

  • 如果某对象通过一串强引用链接可到达,即使内存不足,也不会回收该对象

4.2、软引用

  • 用来描述一些非必须,但仍有用的对象。

  • 内存足够时,软引用对象不会被回收,

  • 只有在内存不足时,系统会回收软引用对象,通常用于实现缓存

4.3、弱引用

  • 随时可能被垃圾回收器回收,无论内存是否足够,只要JVM开始进行垃圾回收,那些被弱引用关联的对象都会被回收

4.4、虚引用

  • 虚引用是所有引用类最脆弱的一个,如果一个对象持有虚引用,那么这个对象随时可能被回收,甚至不能通过get方法来获得其指向的对象。

  • 虚引用唯一的作用是,当其指向的对象被回收后,自己被加入到引用队列,用做记录该引用指向的对象已被销毁

  • 跟踪对象被垃圾回收的活动

4.5、被引用的对象不一定能存活

  • 得看Reference类型,弱引用在GC的时候会被回收,软引用在内存不足的时候,即OOM之前被回收,所以被引用的对象不一定就能存活,除非你是强类型

  • 但如果没有被引用,即没有Reference Chain的对象,一定会被回收

5、类的加载过程

5.1、加载

5.1.1、类的加载主要的职责

将.class文件读入内存(JDK1.7及之前为JVM内存,JDK1.8及之后为本地内存),并在堆内存中为之创建Class对象,作为.class进入内存后的数据的访问入口。

在JDK1.7及以前,Hot Spot JVM(普遍在用的JVM)存在一块叫做方法区的内存,也称之为永久代,这块区域用于存放类的元数据信息,包括类的字段、版本、方法等,这块区域,可以理解为.class文件进入内存后的位置。

在JDK1.7时,将常量池从方法区移除,在堆内存开辟了一块空间作为常量池,有人说这是为取消方法区做的准备。

在JDK1.8,取消了方法区,取而代之的是元数据区,该元数据区并非JVM内存,而是本地内存。

5.1.2、为何取消方法区?

5.1.2.1、官方说法

移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代

5.1.2.2、现实使用中存在问题

方法区存储类的元数据信息,我们不清楚一个程序到底有多少类需要被加载,且方法区位于JVM内存,我们不清楚需要给方法区分配多大内存,太小容易PermGen OOM,太大,在触发Full GC时又极其影响性能,同时还存在一些内存泄露的问题

5.2、连接

连接分为验证、准备、解析三个阶段

5.2.1、验证

该阶段主要是为了保证加载进来的字节流符合JVM的规范,不会对JVM有安全性问题,格式验证、语义检查、字节码验证、符号引用验证

其中有对元数据的验证,例如检查类是否继承了被final修饰的类

还有对符号引用的验证,例如校验符号引用是否可以通过全限定名找到

或者检查符号引用的权限(private、public)是否符合语法规定等

5.2.1.1、字节码验证

  • 检查当前class文件的版本和JVM的版本是否兼容

  • 检查当前代码是否会破坏系统的完整性

  • 检查当前代码是否有堆栈溢出的情况

  • 检查当前代码中的参数类型是否正确

  • 检查当前代码中的类型转换操作是否正确 验证通过后,再确定哪些代码是解释执行的,哪些代码是JIT即时编译执行

解释执行:

class文件内容,需要让JVM进行解释,解释成计算机可以执行的代码。

整体效果就是JVM解释一行,代码就执行一行代码。

所以如果Java代码全是这样的运行方式的话,效率会稍低一些

JIT(Just In Time)即时编译:

执行代码的另一种方式,JVM可以把Java中的 热点代码 直接编译成计算机可以运行的代码,

这样接下来再调用这个热点代码的时候,就可以直接使用编译好的代码让计算机直接运行,可以提高运行效率

5.2.2、准备

准备阶段的主要任务是为类的类变量开辟空间并赋默认值。

  • 静态变量是基本类型(int、long、short、char、byte、boolean、float、double)的默认值为

  • 静态变量是引用类型的,默认值为null

  • 静态常量默认值为声明时设定的值 例如:public static final int i = 3; 在准备阶段,i的值即为3

5.2.3、解析

该阶段的主要职责为将Class在常量池中的符号引用转变为直接引用,此处针对的是静态方法及属性和私有方法与属性,因为这类方法与私有方法不能被重写,静态属性在运行期也没有多态这一说,即在编译器可知,运行期不可变,所以适合在该阶段解析,

譬如类方法main替换为直接引用,即静态连接,

符号引用即方法名之类的唯一名称标识,

直接引用即内存中的地址串,

例如,一个类的方法为test(),则符号引用即为test,这个方法存在于内存中的地址假设为0x123456,则这个地址则为直接引用。

5.3、初始化

  • 该阶段主要是为类的类变量初始化值,

  • 初始化有两种方式:

  1. 在声明类变量时,直接给变量赋值

  2. 在静态初始化块,为类变量赋值

5.4、类的加载时机

  1. 创建该类的实例

  2. 调用该类的类方法

  3. 访问类或接口的类变量,或为类变量赋值

  4. 利用反射 Class.forName(String name, boolean initialize,ClassLoader loader);

当使用ClassLoader类的loadClass()方法来加载类时,该类只进行加载阶段,而不会经历初始化阶段,使用Class类的静态方法forName(),根据initialize来决定会不会初始化该类,不传该参数默认强制初始化

  1. 初始化该类的子类

  2. 运行main方法,main方法所在类会被加载

5.5、类的加载顺序

先加载并连接当前类

父类没有被加载,则去加载、连接、初始化父类,依旧是先加载并连接,

然后再判断有无父类,如此递归,所以JVM先将Object加载

如果类中有初始化语句,包括声明时赋值与静态初始化块,则按顺序进行初始化

8、静态方法、非静态方法、变量

8.1、静态方法

  • 在静态方法中可以使用静态变量、静态方法,但是不可使用非静态方法、非静态变量,属性就是变量和常量的统称

  • 因为非静态的属性、方法是属于对象的,不属于类,所以在静态方法内,不能判断此时是否已创建对象,所以不能在静态方法中调用非静态的属性、方法

  • 父类的静态方法不可被子类重写

8.2、非静态方法

  • 非静态方法中可以调用所有的,

  • 因为当你调用非静态方法的时候,说明你已经new对象了,因为只有对象才可以调用非静态方法,所以里面什么都可以用

8.3、变量

  • 变量在赋值常量时,一定是先去常量池找是否有对应值的地址,有就把其地址放进来,没有就在常量池创建新的

8.3.1、变量的分类

  • 成员变量(全局变量):静态变量、实例变量

  • 局部变量

8.3.2、声明位置

  • 静态变量:在类中的方法体外,有static修饰符

  • 实例变量:在类中的方法体外,无static修饰符

  • 局部变量:方法体中或方法的参数列表中

8.3.3、在内存空间中存储的位置

  • 静态变量:方法区

  • 实例变量:堆

    • 局部变量:栈

8.3.4、生命周期

  • 静态变量:和类的生命周期一样,因为它的值是该类对象所共享的,早于对象存在

  • 实例变量:和对象的生命周期一样,随着对象被创建而产生,随着对象被GC回收而消亡,每个对象的实例变量是独立的

  • 局部变量:和方法的生命周期一样,随着方法被调用而产生,随着方法结束而消亡,每次方法调用都是独立的

8.3.5、作用域

  • 成员变量(全局变量): 在本类中,静态方法或静态代码块中无法使用非静态的,其余都可以使用;

在其他类中,可否使用看其修饰符(public、protected、default、private)

  • 局部变量:在其定义的区块外就失效

8.3.6、默认值

  • 成员变量:有默认值

  • 局部变量:没有默认值,必须初始化

9、结构

9.1、顺序结构

正常的代码流程

9.2、分支结构

9.2.1、if、if-else、if-else if、if-else if-else

9.2.2、switch-case-default

如果找到对应的case后,其内代码块没有break,就会一直执行下面的所以case中的代码块,直到碰到break或全执行完(default也会执行)

若没找到对应的case就执行default

9.3、循环结构

  • 退出循环:break

  • 进入下次循环:continue

9.3.1、for、foreach(for-:)

for (初始化语句A;  条件判断语句B;  条件控制语句C) {
  循环体语句D;
}
  • 若为无限循环,那么记住循序是:ABDCBDCBDC......

9.3.2、while、do-while

  • 区别是while是先判断,再执行代码块;

  • do-while是先执行再判断

do {
            
   } while (condition);

注意do-while的结尾有 ;

9.3.3、label标签

test1: for (int i = 0; i < 3; i++) {
  System.out.println(i + "----------------------");
  
  test2: for (int j = 0; j < 3; j++) {
    System.out.println(j);
    if (j == 2) {
      break test1;
    }
  }
}

因为break test1,即结束test1这个循环,结果是: 0----------------------

0

1

2

10、Random

10.1、Math的double随机

double r1 = Math.random() * 100 + 1;

10.2、Random类

Random r2 = new Random();
System.out.println( r2.nextInt(100) + 1 );

11、小方法、小知识

11.0、小知识

11.0.1、java中的数组对象和java.util.Arrays类是什么关系

  • java.util.Arrays类是数组的辅助类,该类中有很多静态方法可以用来操作数组对象,例如给数组元素排序、查找数组中某个元素的下标等等

11.0.2、带src的官网安装包代表什么

代表是源码

11.0.3、main函数可以有的形式

  • 简而言之,public static可以互换,String[] args就是数组,也可以变换

public static void main(String args[]) //代码1
static public void main(String[] args) //代码2
public void static main(String[] args) //代码3  这个是错的
static public void main(String... args) //代码4
public void main(String args[]) //代码5  这个是错的
public static void main(String... args) //代码6
public static void main(String[] test) //代码7

11.0.5、\r、\n的区别

  • \r是将光标移动到这一行的行首,比如print("hello\rwor"),结果是worlo

  • \n是真的换行,光标移动到下一行的行首

11.0.6、关于Integer

  • Integer如果你初始化的时候给他byte范围的值,它会用缓存的,即常量池

  • Integer 的 valueOf 和 parseInt方法是不同的,valueOf 会判断这个值是否在 byte 范围内,在就使用缓存,不在就 new 对象;parseInt 方法则不会去使用缓存。

11.0.7、同一个工程启动多次

  1. 点开这个

  1. 勾选 Allow parallel run

  2. 在 Environment下的 VM options定义启动端口 -Dserver.port=8001

  3. 然后每启动一个修改这个参数就好了

11.1、String相关方法

11.1.1、大小写转换方法

String s1="haha".toUpperCase(); //String全变成大写
String s2="HaHa".toLowerCase(); //String全变成小写

11.1.2、截取子串

String s2=s.substring(2, 4); //包含2,不包含4

11.1.3、String 与 int 的相互转换

String s1 = Integer.toString(10); //int变String
String s2 = String.valueOf(10); //int变String
String s3 = 10 + ""; //int变String
​
int i1 = Integer.parseInt(s); //String变int
int i2 = Integer.valueOf(s); //String变int

11.1.4、将String变成char[]数组

char[] c = s.toCharArray();

11.1.5、判断String是否包含指定字符

boolean flag = "Hello".contains("o");

11.1.6、去除String的前后空格

str = str.trim();

11.1.7、替换String中指定的内容

  • 直接一个点在λ表达式和其他地方有特殊含义,所以要用空括号引起来

  • 替换都用[]框起来

packagePath = packageName.replaceAll("[.]", "\\\\");

11.1.8、StringBuilder

@Test
public void stringBuilderTest() {
  final StringBuilder sb = new StringBuilder("Hello,");
  sb.append("World!").append("Hello,").append("Chw");
  System.out.println(sb);
}

11.1.9、将字符串转换成十进制,后面的数字是原来的进制

Integer.parseInt(str.substring(0, 4), 16)

11.1.10、将字符串转换成char数组,toCharArray()方法

11.1.11、Scanner的nextLine方法的弊端

  • nextLine()会把 nextInt(),next(),nextDouble(),nextFloat()的结束换行符作为字符串读入,进而不需要从键盘输入字符串nextLine便已经转向了下一条语句执行

11.2、类的各种方法

11.2.1、类型判断

返回一个引用在运行时所指向的对象,具体类型是什么

该方法是native修饰的本地方法,不是Java语言实现的

子类中不能重写getClass,只可调用

getClass()
11.2.1.1、是否为基本类型
.isPrimitive()
11.2.1.2、是否为接口
.isInterface()
11.2.1.3、是否为数组
.isArray()

11.2.2、获取类的信息

11.2.2.1、获取权限类名
.getName()
11.2.2.2、获取简单类名
.getSimpleName()
11.2.2.3、获取包名
.getPackage()
11.2.2.4、获取父类
.getSuperclass()
11.2.2.5、获取类实现的所有接口
Arrays.toString(c.getInterfaces())
11.2.2.6、获取类的修饰符
Modifier.toString(c.getModifiers())
//直接c.getModifiers(),0是default,1是public,2是private,4是protected
11.2.2.7、判断c1的类型是否是c2的类型的父类
c1.isAssignableFrom(c2)
11.2.2.8、获取类中的public修饰的属性,也包含从父类中继承过来的public属性
Field[] getFields()
Field getField(String name)
11.2.2.9、获取类中声明的属性(包含私有的),但是不能获取从父类中继承来的属性
Field[] getDeclaredFields()
Field getDeclaredField(String name)
11.2.2.10、获取属性的类型
.getType().getName() //如果直接getType,是获取:class 权限类名
11.2.2.11、获取属性的名称
.getName()
11.2.2.12、获取类中的public修饰的方法,也包含从父类中继承过来的public方法
Method[]  getMethods()
Method  getMethod(String name, Class<?>... parameterTypes)
11.2.2.13、获取类中方法(包含私有的),但是不能获取从父类中继承来的方法
Method[]  getDeclaredMethods()
Method  getDeclaredMethod(String name, Class<?>... parameterTypes)
11.2.2.14、获取方法的返回类型
.getReturnType().getSimpleName()
11.2.2.15、获取方法的参数列表
Class[] paramArr = method.getParameterTypes();
11.2.2.16、获取方法的抛出异常类型
Class[] exceptionArr = method.getExceptionTypes();
11.2.2.17、获取当前类中的public构造器
public Constructor<?>[] getConstructors()
public Constructor<T> getConstructor(Class<?>... parameterTypes)
11.2.2.18、获取当前类中的所有构造器,包含私有的
public Constructor<?>[] getDeclaredConstructors()
public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)
11.2.2.19、创建实例化对象
Class c = Student.class;
//调用无参构造器
Object obj = c.newInstance();
//调用有参构造器
Constructor constructor = c.getConstructor(String.class, int.class);
Object obj = constructor.newInstance("tom",20);
11.2.2.20、获取注解
//获取类上面的所有注解
Annotation[] annotations = c.getAnnotations();
​
Method[] methods = c.getDeclaredMethods();
for(Method m:methods){
  //判断当前方法上是否使用了Role注解
  if(m.isAnnotationPresent(Role.class)){
  //获取方法上的Role注解
  Role role = m.getAnnotation(Role.class);
  }
}

11.3、Arrays的方法

11.3.1、Arrays.toString(array)、Arrays.deepToString(array)

11.3.1.1、Arrays.toString(array)

可以输出一维数组的内容,如果是二维数组,那么就会输出地址信息

11.3.1.2、Arrays.deepToString(array)

可以输出二维及以上数组的内容,即会遍历输出,用toString只能输出地址信息,但是用deepToString就可以输出内容了

11.3.2、Arrays.copyOf(a, 10)、Arrays.copyOfRange(a, 2, 10)

11.3.2.1、Arrays.copyOf(a, 10)

可以复制一个数组,数字10是截止下标的意思,但取不到10,也可以说10是新数组的长度,若原数组没有则赋默认值

11.3.2.2、Arrays.copyOfRange( a, 2, 10)

可以复制一个数组,数字2是起始下标,10是截止下标,但取不到10

11.3.3、Arrays.sort(a)

将a数组自动升序排序,

Arrays的排序只有数组能用,集合不可用

11.3.3、Arrays.fill( a, 6)

将a数组用6填充

11.4、System

11.4.1、System.currentTimeMillis()

long startTime = System.currentTimeMillis(); //时间戳,毫秒为单位

11.5、其他小方法

11.5.1、奇偶数判断

(num & 1) == 0    //用来判断是否为奇数,true 代表是偶数

11.5.2、只使用a,b来进行交换

int a = 15;
int b = 3;
​
a = a+b;
b = a-b;
a = a-b;
​
-----------
a = a^b;
b = a^b;
a = a^b;
-----------
a = a*b;
b = a/b;
a = a/b;

12、类、对象、内存、修饰符、关键字、代码块

12.1、类

  • 类是一组相关属性和行为的集合,它是对某一种具体事物的抽象描述

  • 类是对一类事物的描述,是抽象的

  • 类是对象的模板

12.2、对象

  • 一切事物,皆是对象

  • 对象是一类事物的实例,是具体的

  • 对象是类的实体

12.3、内存

public class Student{
  public String name;
  public void sayHello(){
  }
}
​
public static void main(String[] args){
  Student stu = new Student();
  stu.name = "tom";
  stu.sayHello();
}
  1. 类加载过程,把Student.class文件内容加载到方法区中

  2. main方法运行时,整个main方法的代码都被加载到栈区中

  3. 根据类(相当于模板)创建出的对象,都是在堆区中

  4. 对象是具体的,我们可以给它的属性赋一个具体的值,例如 tom

  5. 引用 stu在栈区中,它保存了这个对象在堆区中的地址(0x123),形象的描述为,引用指向对象

  6. =号赋值操作,其实就是把对象的内存地址,赋值给了引用stu

12.4、修饰符

12.4.1、访问修饰符

修饰符本类中同包子类同包非子类不同包子类不同包非子类
public
protected×
default××
private××××

12.4.2、类、方法、变量修饰符

12.4.2.1、static

static和类的生命周期一样,在方法区,不在对象中(静态的尽量少用)

非静态的在堆区,和对象生命周期一样

静态方法不可以被重写,但可以重载

静态是从类加载开始的,非静态是从对象创建开始的

静态是属于类的,所有对象共享的;非静态是属于对象的

所以类加载之后,就可以直接使用类名访问静态属性和静态方法

所以创建对象之后,才可以使用对象访问非静态属性、调用非静态方法

静态方法里面只能访问静态的,除非创建对象,

非静态方法里面什么都可以访问,因为调用非静态方法的时候意味着已经创建了对象

12.4.2.2、final

final修饰的类、方法就不可被继承、重写,例如:String、Integer这些

final修饰的变量只能赋值一次,再次赋值就会报错,

当用final修饰方法内的变量时,你在方法内就不能修改了,因为调用方法传参的时候就是给他赋值了

JVM不会为final修饰的变量赋默认值,所以需要我们手动赋值

  • 声明的同时赋值

  • 静态代码块(给静态变量)或匿名代码块(给变量)中赋值

  • 构造器中赋值(类中出现的所有构造器都要赋值,否则报错) 若用final修饰引用类型变量,则此变量的指向不可改变

final Student stu = new Student();
stu = new Student(); //报错,不可改变指向
12.4.2.3、abstract
  • abstract可以修饰类、方法

  • 抽象类是有构造器的,用来给子类调用,接口就没有构造器

  • 抽象方法的特点:只有方法的声明,没有实现,即{}

  • 子类必须重写父类的抽象方法,但是可以实现,也可以也定义为abstract,然后不实现内容,但是都必须去重写

  • 有抽象方法的类,一定是抽象类

  • 抽象类内可以没有抽象方法,只要有abstract修饰符即可

  • 抽象类内不是只能有抽象方法,可以有普通方法

  • 抽象类不可实例化对象,但可实例化父类引用的子类

A a = new A(); //不可,因为A是抽象类
A ab =new B(); //可以,父类引用指向子类对象
12.4.2.4、interface
  • 接口不是类,三大引用类型:数组、类、接口,他们平级的

  • 接口没有构造器,没有代码块,变量只能public,不可private,会报错

  • 单继承,因为多继承容易产生冲突;多实现

  • 接口就为了封装方法、常量,并且默认都是public的,不可private, 里面的变量都默认是常量,默认就是public static final修饰的,可不写

里面的方法都默认是抽象方法,默认就是public abstract修饰的,可不写

  • 接口中允许编写:静态方法、抽象方法、默认方法、私有方法

  • 接口多实现,那么就可以实现多强转

class Student implements A,B{}
​
class Test{
  public static void main(String[] args){
    A a= new Student();
    B b = (B)a;    //这样是可以的,因为Student实现了这两个接口
  }
}
12.4.2.5、抽象类与接口类的现实区别
  • 抽象类一般用于实现子类必须有的功能,例如吃饭、上厕所等

  • 接口类一般用于功能性技能,例如跳舞、唱歌等

12.4.3、程序控制修饰符

12.4.3.1、instanceof
  • 判断对象是否属于指定的类型

12.5、native(本地)修饰符

一个Native Method就是一个Java调用非java代码的接口。

标识符native可以与所有其它的java标识符连用,但是abstract除外。

这是合理的,因为native暗示这些方法是有实现体的,只不过这些实现体是非java的,但是abstract却显然的指明这些方法无实现体。

一个native method方法可以返回任何java类型,包括非基本类型,而且同样可以进行异常控制。

12.5.1、为何使用Native Method?

12.5.1.1、与java环境外交互

有时Java应用需要与Java外面的环境交互。

这是本地方法存在的主要原因,你可以想想Java需要与一些底层系统如操作系统或某些硬件交换信息时的情况。

本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解Java应用之外的繁琐的细节

12.5.1.2、与操作系统交互

JVM支持着Java语言本身和运行时库,它是Java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。

然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一些底层(underneath在下面的)系统的支持。

这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用Java实现了JRE的与底层系统的交互,甚至JVM的一些部分就是用C写的,

还有,如果我们要使用一些Java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法

12.5.1.3、Sun's Java

Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。JRE大部分是用Java实现的,它也通过一些本地方法与外界交互。

例如:类java.lang.Thread 的 setPriority()方法是用Java实现的,但是它实现调用的是该类里的本地方法setPriority0()。

这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用Win32 SetPriority() API。这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后被JVM调用

12.6、关键字

12.6.1、this关键字

  • this相当于当前类的引用,在一些方法里面this也是方法调用者的意思

12.6.1.1、使用场景
  • 区别成员变量和局部变量

  • 调用本类中的其他非静态方法

  • 调用本类中的其他构造器的

12.6.2、super关键字

super就相当于父类引用

12.6.2.1、使用场景
  • 访问父类中的属性

  • 调用父类的方法

  • 调用父类的构造器

12.7、this、super关键字之间存在的矛盾

  • this、super关键字调用构造器,都必须放在代码段第一行,这就存在矛盾了,

  • 如果不是都调用构造器,那么就是构造器在第一行,不会有矛盾

  • 当一个代码段中即存在this,又存在super时,假如在子类的构造器中调用父类的无参构造器,即super(); 那么可以不写 super(),

  • 因为子类一定是默认先调用父类的无参构造(注意:如果父类中只有有参构造,那么就没有无参构造器了),那么就可以让 this在第一行,那么就又有this调用构造器,又有super调用父类构造器

  • 但是如果super调用父类的有参构造器,this也调用自己的有参构造器,那么他们两个只能调用一个放在第一行

12.8、构造器

12.8.1、默认的无参构造器特点

  • 若类中没有任何构造器,则有默认的无参构造器,

  • 若类中有一个及以上的构造器,则默认的无参构造器消失

12.9、代码块

12.9.1、静态代码块

因为调用时间和类加载时间差不多,所以比对象早,所以可以给静态属性做初始化

public class Demo {
  public static int num;
  
  static{
    num = 10;
  }
}
12.9.1.1、执行时刻

类加载的时候自动执行,

静态代码块只会自动被执行一次,因为JVM在一次运行中,对一个类只会加载一次

12.9.2、匿名代码块

调用时间是创建对象的时候,构造器调用之前,但是匿名代码块能做到构造器也能做,所以使用的不多

public class Demo {
  public int num;
  
  static{
    num = 10;
  }
}
12.9.2.1、执行时刻

创建对象的时候,构造器调用之前,自动执行,

每次创建对象,匿名代码块就会执行,就跟构造器差不多

类加载和创建并初始化对象的顺序过程

Student stu =new Student();
  1. 先连接Student,若其有父类,则下面的所有顺序中,都是先执行父类的,再执行子类,不是说父类的所有都先于子类,而是顺序中先于子类 例如:父类静态代码块 > 子类静态代码块 > 父类匿名代码块 > 父类构造器 > 子类匿名块 > 子类构造器

  2. 对Student类进行类加载,同时初始化类中的静态的属性赋默认值,给静态方法分配内存空间,假如你静态属性那块是在new的static对象,就先new此对象,

  • 但是记住如果new的是自己的类对象,得是static,如果不是static的话new自己就会无限new,有 StackOverflowError,栈溢出错误

  • 假如 new对象的类型就是当前类,那么只是使用其匿名构造块和构造器,不会先去调用静态块,因为这个类的类加载已经开始了,只是还没到静态块,所以当 new当前类的对象的时候,因为类加载已经开始了,所以new的对象直接就去调用构造块和构造器了

  • 但是如果 new两个自己的类对象,自己继承了父类,那么就是:父类静态块 -> 父类匿名块 -> 父类构造器 -> 自己匿名块 -> 自己构造器 -> 父类匿名块 -> 父类构造器 -> 自己匿名块 -> 自己构造器

  • 假如 new别的类型对象,那么就会去加载这个类,然后它的静态块、构造块、构造器都会执行

  1. 执行类中的静态代码块(先父类(如果父类的还没被执行过的话),再自己)

  2. 堆区中分配对象的内存空间,同时初始化对象中的非静态的属性赋默认值

  3. 调用父类的匿名代码块、构造器,如果定义个父类变量,然后一个方法来输出此变量,然后子类来重写这个方法,那么就可以知道有没有显示赋值,一定要重写,让方法去找子类中重新赋值的变量,不然父类的赋值会显示

  4. 对Student类中的属性显示赋值,之前赋默认值是0,false这些,现在是赋你代码中全局变量给的值,例如 public int age =10;

  5. 执行本类的匿名代码块

  6. 执行本类的构造器

  7. =号赋值操作,将对象的堆中的内存地址,赋给栈中的stu,使其保存此引用

13、数组

13.1、声明

数组赋予长度时,必须是已知值的变量,且new出来就有默认值,

int a;
int[] a = new int[a]; //这样是错误的,因为a没有值
​
a=9;
int[] a = new int[a]; //这样是可以的,因为a有值,即使a下面变了
a=10;

13.1.1、正确写法

int[] array;
int array[];
array = new int[3];  ==  int[] array = new int[3];
array = new int[] { 0, 1, 2 }; == int[] array = new int[] { 0, 1, 2 };
int[] array = new int[] { 0, 1, 2 };
int[] array = { 0, 1, 2 }; //不可拆分

13.1.2、错误写法

//不可既给长度又给确定值,因为你一下子给了两个长度,即使长度一样
int[] array = new int[2]{ 0, 1 };
​
//直接给确定值的方式只有在声明的时候才可用
int[] array;
array={ 0, 1 };

13.2、二维数组

//二维数组的每一行的长度可以不一样
int[][] array=new int[2][];
array[0]=new int[10];
array[1]=new int[3];

13.3、可变参数

  • 可变参数必须放到最后一个参数位置

void test(int... a){ }
​
int[] arr = {1,2,3};
t.test();
t.test(1);
t.test(1,2,3,4);
t.test(arr);

int... a就相当于一个可变长度的数组,可接收数组或任意个指定类型数据

14、String对象的内存详解

注意:

  • 只有用双引号加文字的方式,才会利用到内存中的字符串常量池,当然数字可以不加双引号,也会使用字符串常量池

  • 使用+拼接的字符串也会利用字符串常量池,但是参与+号拼接的必须是双引号的形式才可以,不可以是对象

14.1、JVM为提高性能、节省开销做的优化

  • 为字符串开辟了字符串常量池,类似缓存池

  • 创建字符串常量时,先检查字符串常量池中是否存在该字符串,若存在则返回实例的引用,若不存在则实例化创建改字符串,然后放入池中

14.2、==判断

String str1 = "hello";
String str2 = new String("hello");
String str3 = "hello";
​
//String中对equals进行了重写,比较的是字符串中每一个字符是否相等
System.out.println(str1.equals(str2));//true
​
System.out.println(str1 == str2);//false
System.out.println(str1 == str3);//true  因为str1和str3指向同一个对象

14.3、+拼接

只要是""的,那就可以参与字符串常量池的使用,s1+s2是变量连接,没用到字符串常量池

String s1 = "a";
String s2 = "b";
​
//使用+拼接的字符串也会利用字符串常量池,但是有要求:参与+号拼接的必须是双引号的形式才可以。
String s3 = "a"+"b";
String s4 = s1+s2;
System.out.println(s3.equals(s4));//true
//s3指向的是常量池中的"ab"对象
//s4指向的堆区中新建的"ab"对象
//因为s4是由s1和s2使用+号连接得到的,而s1和s2都是变量
//有变量参与拼接字符串,那么就不会使用常量池了
System.out.println(s3 == s4);//false

14.3、final修饰,编译期计算

因为final修饰的值,在编译期间就计算出来了,所以就是"",所以就是使用字符串常量池

final String s1 = "a";
final String s2 = "b";
​
String s3 = "a"+"b";
String s4 = s1+s2;
System.out.println(s3.equals(s4));//true
//s3指向的是常量池中的"ab"对象
//s4指向的是常量池中的"ab"对象
//因为s4是由s1和s2使用+号连接得到的,而s1和s2都是final修饰的【常量】
//常量是固定不会变的,在编译期间就能计算出s4的值,这时候又可是有到字符串常量池了
System.out.println(s3 == s4);//true

14.4、intern()方法

当调用 intern() 方法时,JVM会将字符串添加到常量池中,并返回指向该常量的引用

String s1 = "a";
String s2 = "b";
​
String s3 = "a"+"b";
String s4 = (s1+s2).intern();
System.out.println(s3.equals(s4));//true
//s3指向的是常量池中的"ab"对象
//s4指向的是常量池中的"ab"对象
//intern方法可以在JVM在运行期间,强行使用字符串常量池
//检查当前调用intern方法的字符串,是否在常量池中,
//如果在那么就返回常量池中的这个对象,
//如果不在,那么就把当前这个调用intern方法的字符串存到常量池,供后面的代码使用。
System.out.println(s3 == s4);//true

14.5、String aa = new String("aa");

这行代码其实创建了两个对象:

  1. 堆区指向字符串常量池的常量"aa"的String对象 创建在栈区指向堆区String对象的引用aa

  2. 字符串常量池创建常量对象"aa"

14.6、toUpperCase()、toLowerCase()方法会重新new String对象

  • 正常的String的==判断,只要使用到了字符串常量池,都会是true,但是如果使用了toLowerCase()、toUpperCase()方法,会return一个新的String对象,就会让引用这个方法的字符串重新new String对象,从而不再使用字符串常量池。

14.7、当用String作为方法参数时

  • 不管怎么样,只要是作为方法的参数,那么就会 在栈内 产生一个新的变量,引用相同的值或引用

  • 但是一般修改引用,因为不会使用到常量池,所以是堆内数据发生变化,引用不会改变,但是只要使用到常量池,就会改变引用

14.7.1、将指向常量池的引用的String作为参数

  • 因为栈区 str1 指向 字符串常量池的 a1,调用方法,则参数 str2 = str1后,str2也指向字符串常量池的a1

  • 修改 str2 = a2,因为String是不可变类,所以是在 字符串常量池新建一个字符串 a2,然后让 str2指向这个新的字符串 a2

  • 虽然str2修改了引用,但是 str1还是指向的a1

14.7.2、将指向堆区对象的引用的String作为参数,对象指向常量池

  1. 调用方法,栈区的 str1 与 str2 都指向 堆内存中的 d1对象,d1对象指向字符串常量池的 a1

  2. 然后修改 str2 =a2,因为 String是不可变类,所以是在 字符串常量池新建一个字符串 a2,然后让 str2直接指向这个新的字符串 a2。

    • 注意此时:是修改栈区 str2的引用指向,而不是d1的引用指向,因为是让 str2 = a2,所以是 str2的引用指向 a2,而不是修改 d1的引用

  3. str2虽然一开始指向了 d1,但是 str2修改了指向,指向了 a2,但是d1并没有发生改变,str1也是指向了d1,所以 str1 也是输出 a1

15、Java三大特性

15.1、封装

隐藏内部的实现细节,只对外提供必要的方法

15.1.1、针对的点

  • 属性,方法是否能够被外界直接调用,常见的getter、setter

  • 将业务进行封装,只需暴露对外接口

15.1.2、优点

  • 提高代码的安全性,将重要信息私有化,不对外暴露

  • 提高代码的复用性,将常用的代码或功能封装到方法中,可以反复调用

  • 提高代码的可维护性,封装代码的实现细节,便于修改代码

  • 简化使用者的调用

15.2、继承

  • 子类中的super关键字,是父类的引用

  • 子类的构造器默认调用父类的无参构造器

  • 注意:接口不可以继承类,不可以实现接口,但可以多继承接口

15.2.1、继承的点

  • 子类继承父类的所有除private外的属性和方法

  • 子类不会继承父类的构造器

  • extends只能有一个,单继承;但是 implements可以有多个,多实现

  • new子类对象的时候会默认先调用父类构造器

15.2.2、优点

  • 提高代码复用性

  • 类与类之间产生了关系,此为多态的前提

15.2.3、父子类对象

  • new什么,就是什么类的对象,指向什么堆

  • 而你指定什么类型,就是什么类型的引用 例如:A是父类,B是子类

B ba = new A();
//报错,因为这是子类引用指向父类,是父类的对象,但是却变成子类类型
//子类的对象可以隐式变成父类类型,但是父类对象需要强转成子类类型
A ab = new B();
//这是对的,父类引用指向子类,子类的对象是父类的类型
  • ab可调用的方法、属性,只可以是A、B共有的,

  • 若B有的A没有的属性、方法,ab不可以调用

15.2.4、引用类型的转换

15.2.4.1、向上转型(子类对象 变 父类对象)

就是类型变大,道理还是一样的,可以隐式转换

15.2.4.2、向下转型(父类对象 变 子类对象)

就是类型变小,需要强制转换(子类)

15.2.5、父子类的方法调用顺序

15.3、多态

相同类型的不同对象,调用同一个方法,产生不同的结果

15.3.1、多态的前提

  1. 子类继承父类

  2. 子类重写父类的方法

  3. 父类引用指向子类对象

public class Animal {
  public void eat(){}
}
​
class Cat extends Animal {
  public void eat() {
    System.out.println("我是喵咪,我喜欢吃鱼");
  }
}
​
class Dog extends Animal {
  public void eat() {
    System.out.println("我是狗仔,我喜欢吃骨头");
  }
}
​
public static void main(String[] args) {
  Animal a = new Cat();
  a.eat();
  
  a = new Dog();
  a.eat();
}

15.3.2、多态中的类型转换

父类引用是无法调用到子类中独有的方法的

并且父类引用只能指向一个子类对象,不可强转

public class Person {
}
​
class Student extends Person{
  public void study(){}
}
​
class Teacher extends Person{
}
​
public static void main(String[] args) {
  Person p1 = new Student();
  p1.study(); //编译报错,p1不可调用这个方法
  
  Person p = new Teacher();
​
  //编译通过,运行报错! 错误类型是:ClassCastException
  Student s = (Student)p;
  s.study();
}

16、方法重载和重写

16.1、重载

在同一个类中,多个同名的方法必须有不同的参数列表,它们的返回类型、访问权限可以不同,但重载的决定性因素是:同名方法有不同的参数列表

静态方法可以被重载

参数列表不同,只需满足其中一项即可

  • 参数个数不同

  • 参数类型不同

  • 参数顺序不同

16.2、重写

重写发生在子类中,若没有重写,new一个子类的对象后调用时,调用父类的,若重写了,那么不会去走父类的,而是直接只走子类重写的方法

父类中的 静态、私有 方法不可被子类重写

就算子类中有和父类一样的静态方法,那也不是重写,他们之间没有联系

二同、二小、一大

16.2.1、二同

  • 方法名必须相同

  • 参数列表必须相同

16.2.2、二小

  • 抛出的异常类型 小于或等于 父类的

  • 返回类型 小于或等于 父类的

  1. 如果父类的返回类型是 八大基本类型(byte、short、int、long、float、double、char、boolean),那么子类重写的返回类型只能 等于 父类的返回类型,不能是更小的类型

  2. 如果父类的返回类型是 引用类型,那么子类重写的返回类型可以 小于或等于父类的返回类型, 例如:Animal的子类有Person,父类的返回类型是Animal,那么子类的返回类型要么是Animal,要么是Animal的子类Person

注意:Long、Integer它们虽然是引用类型,但是它们只有精度上面的区别,它们之间并没有父子类的关系,所以就算父类的返回类型是Long,子类的也不能是Integer

16.2.3、一大

  • 访问修饰符 大于或等于 父类的

17、静态导入

import static

17.1、未使用静态导入时

Math.Random()

17.2、使用静态导入时

import static java.lang.Math.random;
Random()

18、常用重写

18.1、toString()

重写toString()可以方便输出,否则就是输出地址,String、Integer这些包装类都帮我们重写好了此方法

18.2、equals()

Object中的equals就是 ==比较两个对象的地址值,

String重写了equals方法,就是比较内容了

重写equals可以实现内容比较,经常与hashCode()进行配合,在集合中

先使用hashCode进行快速判断,相同则使用equals进行细致比较

18.2.1、重写equals需要注意的五个特性

  1. 自反性:对任意引用obj,obj.equals(obj)的返回值一定为true.

  2. 对称性:对于任何引用o1、o2,当且仅当o1.equals(o2)返回值为true时,o2.equals(o1)的返回值一定为true;

  3. 传递性:如果o1.equals(o2)为true, o2.equals(o3)为true,则o1.equals(o3)也一定为true

  4. 一致性:如果参与比较的对象没任何改变,则对象比较的结果也不应该有任何改变

  5. 非空性:任何非空的引用obj,obj.equals(null)的返回值一定为false

18.3、hashCode()

通过hash(散列)算法计算出一个int返回值

18.3.1、对于俩个对象的hashCode值:

  • 相等的俩个对象,hashCode值一定相等

  • hashCode值相同,俩个对象有可能相等,也可能不相等

  • hashCode值不同,俩个对象一定不同

19、泛型

  • 虽然Integer是Object的子类,但是ArrayList<Integer>和ArrayList<Object>之间没有父子类关系,

  • 即当类型作为泛型的指定类型后就没有多态特性了

19.1、泛型类

public class Point<T>{
  T x;
  T y;
  
  public Point(){}
  public Point(T x, T y) {
    this.x = x;
    this.y = y;
  }
}
​
//p1对象中x和y的属性类型都是Integer
Point<Integer> p1 = new Point<Integer>();  //使用
p1.x = 1;
p1.y = 2;
//p2对象中x和y的属性类型都是String
Point<String> p2 = new Point<String>();
p2.x = "1";
p2.y = "2";

19.2、泛型接口

public interface Action<T>{...}
​
//创建匿名内部类
Action<String> a = new Action<String>(){...};

19.3、泛型方法

泛型方法只有定义的时候有<>,使用的时候直接使用

public class Test{
  public <T> T test(T t){...}
}
​
public static void main(String[] args){
  Test t = new Test();
  
  String str = t.test("hello");
  Integer i = t.test(1);
  Double d = t.test(10.5D);
}

19.4、通配符:?

19.4.1、?的优点

  • 使用通配符 ? 来表示泛型的父类型,用来解决,这种情况,并且当集合作为参数时,你想使用 T 要么有泛型类、接口、方法,如果你都不想,那就得用到 ? 了

public void test1(Collection<Integer> c){}
public void test2(Collection<String> c){}
public void test3(Collection<Object> c){}
​
public void test(Collection<?> c){}
  • 因为test1不能转换成test3,所以如果Object类型的集合想使用test1,就必须多定义个test3,

  • 现在用通配符?,就可以共享一个方法了

19.4.2、?的缺点

Collection<?> c = new ArrayList<String>();
  • 因为你的类型是通配的,即 ? ,那么编译器不知道 ? 是什么类型, 只知道可以通配,

  • 如果你存一个固定类型的数,那么就会报错

  • 所以通配符的主要作用就是来得到已经有值的集合,然后进行遍历,这虽然不能添加新数据,但可以取出元素

19.5、泛型边界

19.5.1、上限(? extends 父类)

extends,说明所有?都得是父类的子类,那么就有了上限

List<? extends Number> list;
​
//list可以指向泛型是Number或者Number【子】类型的集合对象
list = new ArrayList<Number>();
list = new ArrayList<Integer>();
list = new ArrayList<Double>();

19.5.1.1、使用场景

在 泛型类、泛型接口、泛型方法、声明变量 中都可以使用

public class Point<T extends Number>{
  private T x;
  private T y;
}
​
interface Action<T extends Person>{
  public void test(T t);
}
​
public <T extends Action> void test(T t);
​
public void test(List<? extends Number> list){...}
List<? extends Number> list = new ArrayList<Long>();  t.test(list);

19.5.2、下限(? super 子类)

super ,说明所有?都得是子类的父类,那么就有了下限

List<? super Number> list;
​
//list可以指向泛型是Number或者Number【父】类型的集合对象
list = new ArrayList<Number>();
list = new ArrayList<Serializable>();
list = new ArrayList<Object>();

19.5.2.1、使用场景

在 泛型类、泛型接口、泛型方法 中都不可以使用

只有在 声明变量 时可以使用

public class Test{
  public void test(List<? super Number> list){...}
}
​
public static void main(String[] args) {
  List<? super Student> list;
  list = new ArrayList<Student>();
  list = new ArrayList<Pesson>();
  list = new ArrayList<Object>();
}

19.6、使用泛型的优点

  1. 代码复用

  2. 降低耦合度

  3. 可读性更高

  4. 类型安全,消除了强制类型转换

  5. 类型不写死,更灵活

19.7、泛型擦除(类型擦除)

  • 编译后,所有泛型统一变成Object类型

  • 泛型类型仅存在于编译期间,编译后的字节码和运行时不包含泛型信息,所有的泛型类型映射到同一份字节码。

  • 使用擦除法的好处就是实现简单、非常容易Backport,运行期也能够节省一些类型所占的内存空间。

  • 而擦除法的坏处就是,通过这种机制实现的泛型远不如真泛型灵活和强大。 Java选取这种方法是一种折中,因为Java最开始的版本是不支持泛型的,为了兼容以前的库而不得不使用擦除法。泛型类型只有在 静态类型检查期间 才出现,在此之后,程序中的所有泛型类型都将被擦除,替换成它们非泛型上界

//编译期间
class Generic<T> {
  private T obj;
  public Generic(T o) {
    obj = o;
  }
  public T getObj() {
    return obj;
  }
}
​
//编译后,所有泛型统一变成Object
class Generic {
  private Object obj;
  public Generic(Object o) {
    obj = o;
  }
  public Object getObj() {
    return obj;
  }
}

因为泛型被擦除成Object类型,那么只有泛型类型不同的地方就会报错

//会报错,因为编译后,都变成了public void run(List list),那么就有两个一样的方法了
public class Test {
  public void run(List<String> list){}
  public void run(List<Integer> list){}
}

#

20、内部类

这样是两个独立的类,不是内部类

public class A{
}
class B{
}

20.1、成员内部类

  • 成员内部类中,不可编写 静态的属性、方法

  • 编译后会产生两个class:A.class、A$B.class

  • 成员内部类可以直接访问外部类的所有属性、方法(包括private)

  • 外部类在new内部类对象后可直接访问所有属性、方法(包括private)

  • 如果成员内部类不是private修饰的,那么在其他类中就可以访问到此类

  • 类的内部可以嵌套类、接口

  • 接口的内部也可以嵌套接口

public class A{
  class B{
  }
}

20.1.1、在其他类访问非private修饰的成员内部类时需要注意的点

  1. 内部类需要import导入

  2. 创建内部类对象时,需要先创建外部类对象,再创建内部类对象

A a =new A();
B b = a.new B();

20.2、静态内部类

  • 静态内部类就比成员内部类多了个static修饰符

  • 只有静态内部类可以定义静态的属性、方法,其他内部类想定义static就得加final

  • 也和成员内部类一样,生成两个class文件

  • 静态内部类中不能访问外部类中的非静态属性、方法,除非你new对象

public class A{
  static class B{
  }
}

20.2.1、在其他类访问非private修饰的静态内部类时需要注意的点

  1. 内部类需要import导入

  2. 创建内部类对象时,直接创建内部类对象,不再依赖外部类对象了

B b = new B();

20.3、局部内部类

  • 局部内部类声明在外部类的方法中,所以其作用范围只在方法中

  • 局部内部类中,访问当前方法中的变量,这个变量必须是final修饰的, JDK1.8后自动变成final了,因为在局部内部类发现不能进行第二次赋值

public class A{
  void Hello(){
     class B{
       
     }
  }
}

20.4、匿名内部类

  • 匿名内部类是一种没有名字的内部类的简化写法,很重要

20.4.1、匿名内部类的两种形式

  1. 利用一个父类,进行声明并创建匿名内部类对象,这个匿名内部类默认就是这个父类的子类型

  2. 利用一个接口,进行声明并创建匿名内部类对象,这个匿名内部类默认就是这个接口的实现类

20.4.2、匿名内部类注意点

  1. 匿名内部类必须依托于一个父类型或者一个接口

  2. 匿名内部类在声明的同时,就必须创建出对象,否则后面就没法创建了

  3. 匿名内部类中无法定义构造器

20.4.3、匿名内部类实例

20.4.3.1、实现父类
  • 创建出对象的方式

public abstract class A {
    public abstract void run();
​
    public static void main(String[] args) {
        A a = new A() {
            @Override
            public void run() {
                System.out.println("匿名内部类中的实现");
            }
        };
        a.run();
    }
}
  • 直接new的方式

public abstract class A {
    public abstract void run();
​
    public static void main(String[] args) {
        new A() {
            @Override
            public void run() {
                System.out.println("匿名内部类中的实现");
            }
        }.run();
    }
}
20.4.3.2、实现接口
  • λ表达式(λ表达式只能用于实现方法,不能重写方法)

public interface A {
    void run();
}
​
public class B {
    public static void main(String[] args) {
        A a = () -> {
            System.out.println("匿名内部类中的实现");
        };
        a.run();
    }
}
  • 直接new的方式

public abstract class A {
    public abstract void run();
​
    public static void main(String[] args) {
        new A() {
            @Override
            public void run() {
                System.out.println("匿名内部类中的实现");
            }
        }.run();
    }
}
  • 创建出对象的方式

public abstract class A {
    public abstract void run();
​
    public static void main(String[] args) {
        A a = new A() {
            @Override
            public void run() {
                System.out.println("匿名内部类中的实现");
            }
        };
        a.run();
    }
}

20.5、内部类的使用选择

  1. 考虑这个内部类,如果需要反复的进行多次使用(必须有名字)

  • 在这个内部类中,如果需要定义静态的属性、方法,选择静态内部类

  • 在这个内部类中,如果需要访问外部类的非静态属性、方法,选择使用成员内部类

  1. 考虑这个内部类,如果只需要使用一次(可以没有名字)

  • 选择使用匿名内部类

  1. 局部内部类,几乎不会使用

21、集合

去重是通过重写hashCode、equals

排序是通过自然排序、比较器排序,但排序有去重功能,因为 - 为0则相等

重要组成部分:接口、实现类、数据结构

五种遍历方式:

  1. for循环:只适用于List

  2. foreach循环:只适用于Collection

  3. iterator迭代器:若是Map的迭代器,需要封装成Map.Entry<k,v>

  4. foreach的λ表达式

  5. stream

l1.forEach((e) -> {if (e % 2 == 0) {l2.add(e);} } ); //List例子
​
map1.forEach((k, v) -> System.out.println(k + "出现了" + v + "次"));
  1. stream的filter

//List例子,Map只要把(e)变成(k,v)就好了
l2 = l1.stream().filter((e) -> e % 2 == 0).collect(Collectors.toList());

21.1、数据结构

21.1.1、栈(Stack)

又称堆栈,先进后出,只能从尾部进行插入、删除等操作,跟坑一样

  • 下面那端叫栈底,即不可操作那端

  • 上面那端叫栈顶,即可操作那端 入栈也叫压栈,把数据存到栈顶

出栈也叫弹栈,把栈顶位置的数据取出

JVM栈区中,把main方法标注在最低端位置

21.1.2、队列(Queue)

先进先出,只允许入口进行插入,出口进行删除,跟火车隧道一样

21.1.3、数组(Array)

  • 内存一块连续的空间,元素根据下标索引依次存储中

  • 查询快、插入删除慢,

  • 因为是通过下标索引进行的,所以找下标就直接找到,但是增删要移动后面的元素

21.1.4、链表(Linked List)

  • 由一个个node节点组成,

  • 单向链表:每个节点对象存储数据、指向下一个node节点对象的引用

  • 双向链表:每个节点对象存储指向上一个node节点对象的引用、数据、指向下一个node节点对象的引用

  • 查询慢、增加删除快,

  • 因为是通过引用连接的,所以查询的话要一个个找下去,而增删只要换节点就好了

21.1.5、红黑树(Red/Black Tree)

Red/Black Tree Visualization

二叉树中的平衡二叉B树,动态平衡

21.1.5.1、概要

每次来一个新元素,比根节点小的放左子树,比跟节点大的放右子树,

然后继续递归地放,直至一个合适的位置,

放完后,如果红黑树左右子树高度不一,就进行左旋或右旋来平衡高度

21.1.5.2、约束
  • 根节点必须是黑色

  • 其他节点可以是红色的或者黑色

  • 叶子节点(特指null节点)是黑色的

  • 每个红色节点的子节点都是黑色的

  • 任何一个节点到其每一个叶子节点的所有路径上黑色节点数相同

21.1.6、哈希表

JDK1.8之前是采用数组+链表的方式,

现在是 数组+链表+红黑树 实现的

  • 先根据哈希值将相同哈希值的元素存在同下标的数组中

  • 若数组某下标中的元素个数小于8,就使用链表;大等于8就使用红黑树

21.2、Collection单列集合

booleanadd(E e)是否成功添加指定元素
booleanaddAll(Collection c)是否成功添加指定集合
voidclear()清除所有元素
booleancontains(Object o)判断集合中是否包含指定元素
booleancontainsAll(Collection c)判断集合中是否包含指定集合的元素
inthashCode()返回此集合的哈希码值
booleanequals(Object o)比较此集合与指定对象是否相等
booleanisEmpty()集合是否为空
Iterator<E>iterator()返回此集合的迭代器
intsize()返回此集合中的元素个数
booleanremove(Object o)移除指定元素
booleanremoveAll(Collection c)移除指定集合的元素
Object[]toArray()返回包含此集合中所有元素的数组
l1.sort(Comparator.naturalOrder()); //比较器排序,升序
l1.sort(Comparator.reverseOrder()); //比较器排序,降序
Collections.sort(l1); //自然排序,升序

21.2.1、List

带下标索引,有序(按照存储顺序),元素可重复

voidadd**(int index, E e)**在指定下标存元素
Objectget(int index)获取指定下标的元素
voidset(int index, E e)替换指定下标的元素
intindexOf(Object o)返回从前往后第一次碰到此元素的下标
intlastIndexOf(Object o)返回从后往前第一次碰到此元素的下标
List<E>subList(int fromIndex,int toIndex)从指定下标截取集合
21.2.1.1、Linked List
voidaddFirst(E e)将元素添加在链表的头部
voidaddLast(E e)将元素添加在链表的尾部
ObjectgetFirst()获取链表的头
ObjectgetLast()获取链表的尾
Objectpop()返回并删除头部
voidpush(E e)将元素添加在链表的头部
ObjectremoveFirst()返回并删除头部
ObjectremoveLast()返回并删除头部
21.2.1.2、ArrayList
  • 默认初始长度:10

  • 每次动态扩容:现长度 * 1.5

21.2.2、Set

  • 无下标索引,无序(按照ASCII码排序),元素唯一

  • Set需要重写hashCode、equals这两个方法,方便去重, 否则new两个内容一样的对象,Set不会去重

21.3、Map双列集合

Objectput(Obejct k, Object v)把指定键值对放到map中,<br>若key存在则替换value值
Integerreplace(Obejct k, Object v)替换对应key的value,<br>成功会返回替换个数,<br>没有对应key就会返回null,不会报错
voidputAll(Map m)把指定map放进去
booleancontainsKey(Object key)是否包含指定key值
booleancontainsValue(Object value)是否包含指定value值
voidclear()清除map中的所有元素
Objectget(Object key)获取指定key的value
Objectremove(Object key)移除指定key的键值对
intsize()获取map中的元素个数
booleanisEmpty()判断map是否没有元素
Set<Object>keySet()返回map中的所有key
Collection<Object>values()返回map中的所有value
Set<Map.Entry<K,V>>entrySet()将map中的键值对封装成Entry对象,再放到Set集合中
ObjectgetOrDefault(Object k, v)获取指向key的value,<br>若key不存在则创建key,<br>并给value赋值v

21.3.1、Map.Entry<k, v>

Set<Map.Entry<String, Double>> entrySet = hashMap.entrySet();
for (Map.Entry<String, Double> entry : entrySet) {
  System.out.println(entry.getKey() + " : " + entry.getValue());
}

21.4、Tree

  • 比较器排序的优先级大于自然排序

  • this对象 - 参数对象是升序,反之是降序,

  • 相减为0则相等,会去重 排序的前提:一定可以比较出数据的大小

TreeSet、TreeMap都是为排序产生的,他们有两种排序方法

注意:在使用TreeSet、TreeMap的时候你使用的泛型的指定类型必须有自然排序或比较器排序中的任意一个,否则报错

  • 你若不指定泛型类型,那么就是Object,已帮我们写了,若是Integer、String这些,也是已经帮我们写了,

  • 但是你如果指定的是你自己定义的类,那么这个类必须有其中一个排序

21.4.1、自然排序(内在排序)

适用于已存在实体类的时候

  1. 需要一个实体类去implements Comparable,

  2. 然后重写int compareTo(Object obj)方法,方法体自定义

  3. 使用的时候是默认使用,直接创建集合对象就可以

TreeSet<Teacher> teachers = new TreeSet<Teacher>();
@Override
public int compareTo(Object obj) {
  if (obj instanceof Teacher) {
    Teacher teacher = (Teacher) obj;
    if (!this.getName().equals(teacher.getName())) {
      return this.getName().compareTo(teacher.getName());
    } else {
      if (!(this.getAge() == teacher.getAge())) {
        return this.getAge() - teacher.getAge();
      }
      return this.getId() - teacher.getId();
    }
  }
  return 0;
}

21.4.2、比较器排序(覆盖排序、外在排序)

适用于没有实体类的时候,即没有实体类去实现Comparable接口

  1. 可以在下面定义个独立类,让它implements Comparator,可用λ表达式(t1,t2)->t1-t2

  2. 然后重写int compare(Object o1, Object o2)方法,方法体自定义

  3. 使用的时候需要给集合构造器传参

TreeSet<Teacher> teachers = new TreeSet<Teacher>(new MyComparator2());
class MyComparator2 implements Comparator {
  @Override
  public int compare(Object o1, Object o2) {
  if (o1 instanceof Teacher && o2 instanceof Teacher) {
    Teacher t1 = (Teacher) o1;
    Teacher t2 = (Teacher) o2;
 
    if (!t1.getName().equals(t2.getName())) {
      return t1.getName().compareTo(t2.getName());
    } else {
      if (!(t1.getAge() == t2.getAge())) {
        return t1.getAge() - t2.getAge();
      }
      return t1.getId() - t2.getId();
    }
  }
  return 0;
}

21.5、Collections工具类

  • fill方法:使用指定元素替换指定列表中的所有元素

  • max方法:根据元素的自然顺序,返回给定集合的最大元素

  • min方法:根据元素的自然顺序,返回给定集合的最小元素

  • reverse方法:反转集合中的元素

  • sort方法:根据元素的自然顺序,对指定列表按升序进行排序

  • shuffle方法:使用默认随机源对指定列表进行置换

  • addAll方法:往集合中添加一些元素

  • synchronizedCollection:把非线程安全的Collection类型集合,转为线程安全的集合

  • synchronizedList:把非线程安全的List类型集合,转为线程安全的集合

  • synchronizedSet:把非线程安全的Set类型集合,转为线程安全的集合

  • synchronizedMap:把非线程安全的Map类型集合,转为线程安全的集合

22、枚举

  • 枚举类是种特殊的类,自动使用final修饰,所以不能被其他类继承,并且枚举类已经继承了java.lang.Enum,则也不能继承别人了,单继承

  • 枚举类中定义的对象,是默认public static final修饰的常量,并且这些常量会在静态代码块中做初始化,类加载时就会创建所有的对象

  • 枚举类中的第一行代码,要求一定是指定枚举对象的个数和名字,同时最后面加分号,在这行代码下, 才可以定义枚举类的属性和方法

  • 枚举类有默认的私有构造器,也可以自定义构造器,可以重载,但是构造器的访问修饰符只能是private,默认也是private

  • 所以我们在枚举类外无法创建对象,因为private

  • 我们第一行写的对象中有什么参数,就使用对应的构造器,因为我们在外面不能创建对象,所以必须一开始就决定使用什么构造器,传什么参

  • 枚举类能 使用构造器、定义成员变量、方法、抽象方法、实现接口,

  • 如果枚举类内定义了抽象方法,那么必须让自己的对象去实现方法,

  • 实现抽象方法有两种方式:

  1. 统一实现,不在对象的{}内实现 aa(){...}

  2. 在每个对象内实现

  • 枚举的构造器会一次创建所有的枚举对象,对象后面不加 (参数) 的话就默认使用无参构造器

22.1、继承 java.lang.Enum来的方法

String name()  //获取这个枚举对象的名字
int ordinal()  //获取这个枚举对象的编号。默认从0开始
​
枚举类型[] values()  //返回此枚举类型的所有对象
枚举类型 valueOf(String s) //获取指定名字的枚举对象 

23、注解

用注解可以对Java中的某一个段程序进行说明或标注,并且这 个注解的信息可以被其他程序使用特定的方式读取到,从而完成相应的操作

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {
    String print() default "MyTest run";
}
​
//使用
@MyTest(print="自定义MyTest的输出")  或  @MyTest("自定义MyTest的输出")

23.1、注解和注释的区别

  1. 注解是给其他程序看的,通过参数的设置,可以在编译后class文件中【保留】注解的信息,其他程序读取后,可以完成特定的操作

  2. 注释是给程序员看的,方便程序员快速了解代码的作用或结构,无论怎么设置,编译后class文件中都是【没有】注释信息的

23.2、元注解

元注解也就是对注解进行基本信息设置的注解

  • @Target:用于描述注解的使用范围,例如用在类上面还是方法上面

  • @Retention:用于描述注解的保存策略,是保留到源代码中、Class文件中、还是加载到内存中

  • @Documented:用于描述该注解将会被javadoc生产到API文档中

  • @Inherited:用于表示某个被标注的类型是被继承的,如果一个使用了@Inherited修饰的annotation类型被用于一个class,则此annotation将被用于该class的子类

23.3、使用范围参数,使用在 @Target({})中

  • TYPE:使用在类、接口、注解、枚举等类型上面

  • METHOD:使用在方法上面

  • PARAMETER,使用在方法的参数前面

  • FIELD:使用在属性上面

  • CONSTRUCTOR,使用在构造器上面(了解)

  • LOCAL_VARIABLE,使用在局部变量上面(了解)

  • PACKAGE,使用在包上面(了解)

  • ANNOTATION_TYPE,使用在注解类型上面(了解)

  • TYPE_PARAMETER,使用在声明泛型参数前面,JDK1.8新增(了解)

  • TYPE_USE,使用在代码中任何类型前面,JDK1.8新增(了解)

23.4、保存策略(保持阶段)参数,使用在@Retention()中

  • SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃 如果只是做一些检查性的操作,比如 @Override 和@SuppressWarnings

,使用SOURCE 注解

  • CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃 如果要在编译时进行一些预处理操作,比如生成一些辅助代码,就用 CLASS注解

  • RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在 如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解,比如@Deprecated使用RUNTIME注解

因为RUNTIME的生命周期最长,所以其他俩种情况能作用到的阶段,使用RUNTIME也一定能作用到

24、反射

  • 反射是java中提供的一种机制,它允许我们在程序运行的时候,动态获取一个类中的基本信息,并且可以调用类中的属性、方法、构造器

  • 反射机制就像是镜像、克隆人,它可以直接去获得private的属性、方法

  • 一个类有多个组成部分,例如:成员变量、方法、构造方法等,反射就是加载类,并解剖出类的各个组成部分

  • API:Class、Field、Method、Constructor

24.1、反射主要提供的功能

  1. 生成动态代理

  2. 在运行时判断任意一个对象所属的类

  3. 在运行时构造任意一个类的对象

  4. 在运行时判断任意一个类所具有的成员变量和方法

  5. 在运行时调用任意一个对象的方法

24.2、反射的应用场景

  1. 模块化的开发,通过反射去调用对应的字节码;

  2. 动态代理设计模式也采用了反射机制,

  3. Spring框架

  4. JDBC 的数据库的连接

24.3、反射机制的优缺点

24.3.1、优点

  1. 增加程序的灵活性,避免将程序写死到代码里。

  2. 代码简洁,提高代码的复用率,外部调用方便

  3. 对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法

24.3.2、缺点

  1. 性能问题

  2. 使用反射会模糊程序内部逻辑

  3. 安全限制

  4. 内部暴露

24.4、反射使用全步骤

  • 泛型<>都可以不写,写了也行

  • 无参你可以加个 , 然后写个null,你也可以不写 ,null

24.4.1、获取类镜像

  1. 数据类型.class //类型名称直接获取

  2. 对象.getClass() //不推荐使用

  3. Class.forName("全限定名") //Class类中的静态方法,推荐使用

Class<User> userClass = User.class;
Class<?> userClass = Class.forName("反射.User");或Class<User> userClass = (Class<User>) Class.forName("反射.User");

24.4.2、使用反射镜像直接或间接创建对象

24.4.2.1、通过镜像使用无参构造器创建对象(可以使用,但方法已过时)
User user = userClass.newInstance();
24.4.2.2、通过镜像获取对应构造器,使用构造器创建对象
Constructor<User> constructor = userClass.getConstructor();//无参的构造器
Constructor<User> constructor = userClass.getConstructor(null);
​
Constructor<User> constructor = userClass.getConstructor(int.class, String.class, String.class);
User user = (User) constructor.newInstance(1, "Eris", "wbzdmm");

24.4.3、使用类镜像获取类中的参数

24.4.3.1、获取类中的所有public的属性,包括从父类继承得到的
//获取指定名字的public修饰的属性
Field field= userClass.getField("password");
​
//获取所有的public修饰的属性
Field[] fields = userClass.getFields();
24.4.3.2、获取类中的所有的属性,包括private的,但没有从父类继承得到的
//获取指定名字的属性
Field field= userClass.getDeclaredField("password");
​
//获取所有的属性
Field[] fields = userClass.getDeclaredFields();
24.4.3.3、set属性值、get属性值、设置private属性可被修改
password.setAccessible(true);  //设置password这个私有属性可以被set值
​
//因为这都是变量,所以get、set需要对象参与
password.set(user, "wbzdmm");  //set指定对象的password值
System.out.println(password.get(user));  //get指定对象的password值

24.4.4、使用类镜像获取类中的方法

24.4.4.1、获取类中的所有public的方法,包括从父类继承得到的
//获取指定名字、指定参数个数和类型的public修饰的方法
Method doSomething0 = userClass.getMethod("doSomething");
Method doSomething3 = userClass.getMethod("doSomething", int.class, String.class, String.class);
​
//获取所有的public修饰的方法
Method[] methods = userClass.getMethods();
24.4.4.2、获取类中的所有的方法,包括private的,但没有从父类继承得到的
//获取指定名字、指定参数个数和类型的方法,无参的话也可以不写null
Method doSomething0 = userClass.getDeclaredMethod("doSomething", null);
Method doSomething3 = userClass.getDeclaredMethod("doSomething", int.class, String.class, String.class);
​
//获取所有的方法
Method[] methods = userClass.getDeclaredMethods();
24.4.4.3、invoke使用方法
doSomething3.invoke(user, 1, "Eris", "wbzdmm");//invoke指定对象、指定参数的doSomething3方法

24.4.5、使用类镜像获取类中的构造器

24.4.5.1、获取类中的所有public的构造器
//获取指定参数个数和类型的public修饰的构造器
Constructor c0=userClass.getConstructor();//也可以不指定泛型类型,不要参数
Constructor<User> c3=userClass.getConstructor(int.class, String.class, String.class);
​
//获取所有的public修饰的构造器
Constructor[] cs = userClass.getConstructors();或Constructor<?>[] cs = userClass.getConstructors();
Constructor<User>[] c3 = (Constructor<User>[]) userClass.getConstructors();//指定泛型类型
24.4.5.2、获取类中的所有的构造器,包括private的
//获取指定参数个数和类型的public修饰的构造器
Constructor c0=userClass.getDeclaredConstructor();//也可以不指定泛型类型,不要参数
Constructor<User> c3=userClass.getDeclaredConstructor(int.class, String.class, String.class);
​
//获取所有的public修饰的构造器
Constructor[] cs = userClass.getDeclaredConstructors();或Constructor<?>[] cs = userClass.getDeclaredConstructors();
Constructor<User>[] c3 = (Constructor<User>[]) userClass.getDeclaredConstructors();//指定泛型类型

24.4.6、使用类镜像获取注解

24.4.6.1、获取类上的注解
//获取类上指定类型的注解
Annotation annotation = userClass.getAnnotation(MyZhuJie.class);
​
//获取类上所有的注解
Annotation[] annotations = userClass.getAnnotations();
24.4.6.2、获取方法上的注解
//判断方法上是否使用了指定类型的注解
method.isAnnotationPresent(MyZhuJie.class)
​
//获取方法上指定类型的注解
MyZhuJie myZhuJie= method.getAnnotation(MyZhuJie.class);
​
//获取此注解中的属性值,假设我的注解中有个叫value的String属性
String value = myZhuJie.value();
​
//获取方法上所有的注解
Annotation[] annotations = userClass.getAnnotations();
24.4.6.4、获取属性上的注解
@Test
public void main() throws Exception {
//通过反射获取类
Class clazz = ParameterNameTest.class;
// 获取 "属性变量" 上的注解的值
•
Field[] fields = clazz.getDeclaredFields();
  for(Field field: fields){
      if(field.isAnnotationPresent(Banana.class)){
          Banana bananaAnnotation = field.getAnnotation(Banana.class);
          System.out.println("\"属性变量\"上的注解值获取到第一个 :"
          + bananaAnnotation.length()+ ",第二个:"+ bananaAnnotation.price());
          }
      }
    }
}
24.4.6.4、注解详解
  • directly present:"直接修饰"注解就是指直接修饰在某个元素上的注解,

  • indirectly present:"间接修饰"注解就是指得容器注解的数组中指定的注解;

  • present:并不是"直接修饰"注解和"间接修饰"注解的合集,而是"直接修饰"注解和父类继承下来的"直接注解"的合集;

  • associated:"关联"是"直接修饰"注解、"间接修饰"注解以及父类继承下来的注解的合集;

方法directly presentindirectly presentpresentassociated
getAnnotation
getAnnotations
getAnnotationsByType
getDeclaredAnnotation
getDeclaredAnnotations
getDeclaredAnnotationsByType

24.5、反射中的方法

24.5.0、公共的方法

.getName()  //返回权限名
​
.getSimpleName()  //返回简单名
​
.getModifiers()  //返回访问修饰符对应数字
Modifier.toString(东西.getModifiers())  //返回访问修饰符名

24.5.1、通过类镜像使用的方法

c.isPrimitive()  //判断类镜像是否是基本类型
​
c.isInterface()  //判断类镜像是否是接口
​
c.isArray()  //判断类镜像是否是数组
​
c.getPackage()  //返回类镜像所属的包名
​
c.getComponentType()  //返回类镜像的数组的class对象,如果类镜像不是数组则返回null
​
c.getSuperclass()  //返回父类的class对象,要获取名字就加.getName()
​
Arrays.toString(c.getInterfaces())  //返回类镜像实现的所有接口的名字
​
c1.isAssignableFrom(c)  //判断c1是不是c的父类
​
c.isAnnotationPresent(MyZhuJie.class)  //判断类上是否使用了指定类型的注解

24.5.2、通过得到的属性使用的方法

f.getType().getName()  //返回属性的类型

24.5.3、通过得到的方法使用的方法

m.getReturnType().getSimpleName()  //返回方法的返回类型名字
​
m.getParameterCount()  //返回方法的参数个数
​
Class[] paramArray = m.getParameterTypes();  //返回方法的参数列表数组,对于构造器也适用
​
Class[] exceptionArray = m.getExceptionTypes();  //返回方法抛出的异常类型数组,对于构造器也适用

25、异常

25.1、异常体系

25.1.1、Error

表示错误情况,一般是程序中出现了比较严重的问题,并且程序内部无法进行处理

25.1.2、Exception

  • Exception默认就是编译时异常,除非你指定写RuntimeException

  • 表示异常情况,程序中出了这种异常,大多是可以通过特定的方式进行处理和纠正的,并且处理完了之后,程序还可以继续往下正常运行

25.1.2.1、编译时异常(继承自Exception)

也称为checked exception,编译器在编译期间,会主动检查这种异常, 发现后会报错,并提示我们要对这种异常进行处理(必须去处理)

25.1.2.2、运行时异常(继承自RuntimeExcpetion)

也称为unchecked exception,编译器在编译期间,不会检查这种异常,也不要求我们去处理,但是在运行期间,代码中可能会抛出这种类型的异常

25.2、Throwable中的常用方法

  • printStackTrace():输出到控制台,打印输出当前发送异常的详细信息

  • getMessage():返回异常对象被抛出的时候,所携带的信息,一般是异常的发生原因

  • printStackTrace(PrintWriter s):方法重载,可以指定字符输出流,对异常信息进行输出,可以输出到文件中,所以项目中用的多

  • printStackTrace(PrintStream s):方法重载,可以指定字节输出流,对异常信息进行输出,可以输出到文件中,所以项目中用的多

25.3、异常传播

  • 当发生异常,但是没有处理时,就随着调用链一级级向上传递异常,直至异常被处理或抛给JVM虚拟机,

  • 当抛给JVM了,那么就停止程序,输出异常信息

25.4、处理异常的方法

  1. 不管,让它继续上抛,或者你在方法上只throws抛出你指定的异常类型

  2. 进行try-catch处理,将可能出现异常的代码(此异常代码可以是直接产生的异常,也可以是通过调用方法间接产生的异常)放到try中,然后在catch中捕获异常并处理

25.5、手动抛出异常throw、throws

  • throw是声明可能抛出的异常类型,然后如果throw抛出的是RuntimeException,那么编译器不会显示提醒,

  • 如果throw的是编译时异常,那么必须得由throws抛出或环绕try-catch

  • throws就是抛出异常对象,代表使用我的方法的就得去捕获处理

25.5.1、throw

  • throw new 异常("信息"),在里面可以放信息,然后可以通过catch块中的getMessage获取

25.5.1.1、throw new RuntimeException();

抛出异常后就不会继续执行下面的代码了,直接将程序终止,若包围try-catch,那么就是正常处理完异常后执行

public static void main(String[] args) {
  talk(10, 0);
  System.out.println("main执行");
}
​
public static void talk(int a, int b) {
  if (b == 0) {
    throw new RuntimeException();
  }
  System.out.println("我执行啦");
}
​
输出:只输出异常信息,没有任何自己输出的
25.5.1.2、throw new [xxx]Exception();

抛出编译时异常必须直接处理,并且必须环绕try-catch,有两种方式

  1. throw不环绕try-catch,那么自己不处理,就得让方法抛出去,throws的得是对应处理的异常,然后只要别人使用了该方法的地方,就要环绕try-catch,

  • 并且该方法要么就throws与throw一样的异常,

  • 要么就throws一个最大的Exception,

  • 不可以throw的是AException,但throws的是BException,会报错

  • 也可以throw一个RuntimeException

public static void talk(int a, int b) throws IOException  {
  if (b == 0) {
    throw new IOException();
  }
  System.out.println("我执行啦");
}
​
public static void main(String[] args) {
  try {
    talk(10, 0);
  } catch (IOException e) {
    e.printStackTrace();
  }
  System.out.println("main执行");
}
  1. throw环绕try-catch,自己处理掉,啥事也没有了

25.5.2、throws

throws会创建异常对象,并将该异常对象抛出,让别人去处理

25.6、try-catch-finally块的技巧

  • try-catch的最大好处就是可以捕获并处理异常后,不影响代码执行,虽然try出错下面的代码不执行了,直接去catch块了,但是try-catch外面的代码都是正常运行的

  • try、catch、finally中都还可以继续嵌套try-catch-finally

try {
  可能出现异常的代码
} catch (IOException e) {
  e.printStackTrace();
} catch (NullPointerException e) {
  System.out.println(e.getMessage());
} catch (ClassNotFoundException | NoSuchMethodException) {
  e.printStackTrace();
}  finally {
  System.out.println("chw1");//这句代码一定会被执行
}
System.out.println("chw2");//这句代码也一定会被执行
  • 使用try-catch若try中某处出现异常,则try那处下面的代码就不会执行了,但是catch处理完后不影响try-catch块外面的代码运行

  • catch是一步步向下找,找到可以对应处理的catch块就停下,所以一般都是具体的xxxException在上,最后是最大的Exception

  • 先走try块,如果try块出现异常,则走catch块,最后一定走finally块,若try块正常,那么不会走catch块,一定走finally块

25.6.1、finally块的技巧

  • finally块若有return,那么try块、catch块的return都无效了,最后一定会生效finally块的return

  • finally具体的执行时间其实是:try或catch的return被执行,但return的结果还没有返回,这段时间

25.6.1.1、finally不会执行的情况
  1. 强制停止,就退出JVM了,System.exit(0);

  2. 守护(daemon)线程被中止时

25.6.1.2、finally影响结果的例子
  1. Return1

  • 解析:

  1. i是八大基本类型,所以其赋值都是传值

  2. try块中的return i,因为finally的执行时间是return被执行之后,但是结果未被返回之前,所以此时return i就相当于return 2

  3. finally块虽然改变了i值,但是对try块中的return还是值2

public class Return1 {
  public static void main(String[] args) {
    int i = new Return1().testReturn1();
    System.out.println(i);
  }
​
  private int testReturn1() {
    int i = 1;
    try {
      i++; 
      System.out.println("try:" + i);
      return i;
    } finally {
      i++;
      System.out.println("finally:" + i);
    }
  }
}
​
结果:
try:2
finally:3
2
  1. Return2

  • 解析:

  1. list是引用类型,所以其赋值都是传地址

  2. try块中的return list,因为finally的执行时间是return被执行之后,但是结果未被返回之前,则此时return list就相当于return list的地址引用

  3. finally块改变了list的堆内内容,然后try的结果返回,因为返回的是引用,所以输出的时候的结果就是finally改变之后的

public class Return2 {
  public static void main(String[] args) {
    System.out.println(testReturn2());
  }
​
  private static List<Integer> testReturn2() {
    List<Integer> list = new ArrayList<>();
    try {
      list.add(1);
      System.out.println("try:" + list);
      return list;
    } finally {
      list.add(3);
      System.out.println("finally:" + list);
    }
  }
}
​
结果:
try:[1]
finally:[1, 3]
[1, 3]
  1. Return3

  • 解析:(这个解析得看懂)

  1. String虽然是引用类型,但是其有字符串常量池,有特殊的使用,详情可见 14

  2. try块中的a+="!" 这行代码相当于 a=a+"!",虽然+号可以使用字符串常量池,但是有变量参与,所以不会使用字符串常量池了,而是创建一个String对象,所以这相当于a指向了一个堆内String对象,该对象指向字符串常量池中新增的 hello! 这个字符串,然后return a 其实是 return 这个堆内String对象的引用,该对象内有指向字符串常量池中的 hello! 的引用

  3. finally块也是让a换了个引用指向,指向了堆内的String对象,该对象指向字符串常量池的 hello!world,但是该引用和return 的引用是指向两个不同的堆内String对象的,所以return 的a还是hello!

public class Return3 {
  public static void main(String[] args) {
    System.out.println(testReturn3());
  }
  
  private static String testReturn3() {
    String a = "hello";
    try {
      a += "!";
      System.out.println("try:" + a);
      return a;
    } finally {
      a += "world";  //这样的相加是new了个String对象,指向常量池的Hello!World
      System.out.println("finally:" + a);
    }
  }
}
​
结果:
try:hello!
finally:hello!world
hello!
25.6.1.3、finally总结
  1. finally语句总会执行

  2. 如果try、catch中有return语句,finally中没有return,那么在finally中修改除包装类型和静态变量、全局变量以外的数据都不会对try、catch中返回的变量有任何的影响(包装类型、静态变量、全局变量)

  3. 尽量不要在finally中使用return语句,如果使用的话,会忽略try、catch中的返回语句,也会忽略try、catch中的异常,屏蔽了错误的发生

  4. finally中避免再次抛出异常,一旦finally中发生异常,代码执行将会抛出finally中的异常信息,try、catch中的异常将被忽略

25.7、自定义异常

  • 如果要自定义一个编译时异常类型,就自定义一个类,并继承Exception

  • 如果要自定义一个运行时异常类型,就自定义一个类,并继承RuntimeException 要重写构造器,无参的和有一个String message并调用super(message)

自定义Exception,就是编译时异常,写代码的时候必须处理异常;否则就继承RuntimeException

26、断言(assert)

  • 用它可以在程序中,确认一些关键性条件必须是成立的,否则会抛出 AssertionError 类型的错误

  • 断言(assert)并不是用来代替 if 判断的,而是确认系统中的一些关键性条件是必须成立的,所以 assert 和 if 并不冲突,并且还可以通过给JVM传参数,来控制断言( assert )是否生效

26.1、使用方式

  • 当布尔表达式为true时,断言通过,否则抛出 AssertionError 类型错误

  • 所以,assert后面的布尔表达式必须true才行。(也就是说条件必须成立)

26.2、开启断言

默认情况下,JVM是没有开启断言功能的,需要通过给JVM传参打开此项功能,需要使用 -enableassertions 或者 -ea JVM参数

右键 -> Run As -> Run Configurations -> Arguments -> 写入 -ea

如果去掉-ea参数的话,那么断言(assert)语句,在JVM执行代码的时候,会被直接忽略的

27、线程

  • 进程代表了内存中正在运行的应用程序,计算机中的资源(cpu 内存 磁盘 网络等),会按照需求分配给每个进程,从而这个进程对应的应用程序就可以使用这些资源了

  • 线程开启后,我们无法控制谁先谁后,也无法控制CPU的抢占

  • 一个进程的线程全结束了,那么进程就结束了

  • 一个程序的主进程结束了,那么程序就结束了

  • 调用start()就会调用你自定义的run(),是使用线程机制的,而若你直接调用run()方法,那就是使用你自定义的run方法,不会使用线程机制

  1. 若是继承Thread的方式直接使用run()方法,线程的名字还是Thread-0因为是线程类,但是没有使用线程的并发机制

  2. 若是Runnable的方式直接使用run()方法,线程的名字是main了,因为不是线程类,也没使用线程机制

27.1、程序、进程、线程之间的关系

  • 程序是静态的,例如JVM,一个程序运行可以启动多个进程,一个是主进程,其他的则是守护进程+其他进程,主进程一旦死亡,其他进程跟着死亡,所有进程死亡,程序也死亡

  • 进程是系统进行资源分配和进程调度的最小单位,一个进程可以启动多个线程,若启动了多个线程,那么此程序就是多线程程序

  • 线程是程序执行的最小单元,线程不能独立,因为线程依赖于进程

27.2、线程三要素

27.2.1、CPU

多线程共享CPU,所以需要抢占

27.2.2、代码

  • 多线程中共享run()方法的代码,线程start()后会自动调用run()

  • 注意:共享run()方法的线程要么是

  1. new 同一个Thread的子类

  2. new Thread(同一个Runnable实现类)

27.2.3、数据

局部变量、成员变量、静态变量,两种方式有不同的共享方式

  • 继承Thread:①局部变量不共享②全局变量不共享③静态变量共享

  • 实现Runnable:①局部变量不共享②全局变量共享③静态变量共享

27.3、线程的并发、并行

27.3.1、并发

多个线程抢占单个CPU,同一时刻只能有一个线程使用CPU执行代码

27.3.1、并行

多个线程抢占多个CPU,同一时刻有多个线程在使用CPU执行代码,但同时刻使用CPU的线程数量不超过CPU数

27.4、四种创建线程的方式,继承Thread、实现Runnable、实现Callable、使用线程池

27.4.1、继承Thread,直接变成线程类,创建的对象也直接是线程对象

  • test1 extends Thread,创建对象就直接是线程对象

Thread t1 = new test1;
t1.start();
  • 构造器实现两个,一个无参构造器,另一个带线程名的构造器,super(name)

27.4.1.1、直接创建Thread的匿名内部类
Thread t1 = new Thread() {
  @Override
  public void run() {
    //方法体
  }
 };
t1.start();
27.4.1.2、是线程类,可以直接使用线程的方法

因为是线程类,所以可以直接在类中使用线程的方法,例如getName()

直接  getName()  即可获取当前运行线程的名字

27.4.2、实现Runnable,此时的类不是线程类,创建的对象也不是是线程对象

  • test2 implements Runnable,创建对象不是线程对象,而是Runnable对象

Runnable r = new test2();
Thread t2 = new Thread(r);
t2.start();
27.4.2.1、创建Runnable的匿名内部类
Runnable r = new Runnable() {
  @Override
  public void run() {
    // 方法体
  }
};
Thread t2 = new Thread(r);
t2.start();
27.4.2.2、不是线程类,不可以直接使用线程的方法

因为不是线程类,所以可以在类中不可以直接使用线程的方法,例如getName()

需要  Thread.currentThread().getName()  才可获取当前运行线程的名字

27.4.3、实现Callable接口

  • Callable接口与Runnable接口的功能类似,但提供了比Runnable更加强大的功能

  • Callable的call方法分可以抛出异常,而Runnable的run方法不能抛出异常

  • Callable有返回值,Runnable没有

27.4.4、使用线程池(Thread Pool)

  • 有两种创建线程池的方法

  1. new ThreadPoolExecutor

  2. Executors.方法 使用工厂模式创建线程池

  • 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间

  • concurrent,单词即并发的意思

  • java.util.concurrent ,是JDK中提供的一个专门处理并发的包,里面有一些相关的接口和类,可以在处理线程并发访问的时候使用

  • Executor是一个顶层接口,在它里面只声明了一个 execute 方法,该方法就是用来执行传进去的任务的

  • ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等

  • 抽象类AbstractExecutorService实现了ExecutorService接口

  • ThreadPoolExecutor继承了类AbstractExecutorService,并进行扩展

  • 注意,ThreadPoolExecutor就是JKD中提供的一个线程池类

27.4.4.1、ThreadPoolExecutor
public class ThreadPoolExecutorTest {
    public static void main(String[] args) {
        /*
         * corePoolSize:线程池维护线程的最少数量(核心线程数) maximumPoolSize:线程池维护线程的最大数量
         * keepAliveTime:线程池维护线程所允许的空闲时间 unit:线程池维护线程所允许的空闲时间的单位 workQueue:线程池所使用的缓冲队列
         */
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 2000, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<Runnable>(5));
        // 创建15个线程去执行任务
        for (int i = 0; i < 15; i++) {
            MyRun myTask = new MyRun();
            // 把要执行的任务交给线程池即可
            executor.execute(myTask);
            System.out.println("线程池中线程数目: " + executor.getPoolSize() + ",队列中等待执行的任务数目: " + executor.getQueue().size()
                    + ",已执行完的任务数目: " + executor.getCompletedTaskCount());
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 关闭线程池
        executor.shutdown();
    }
    static class MyRun implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("任务结束");
        }
    }
}
27.4.4.2、Executors
  • java.util.concurrent.Executors,这个是一个工厂类,可以便捷的帮我们生产出线程池对象,同时该类中也提供了一些实用的工厂方法

  1. 工厂方法1

public class FactoryTest1 {
    // 工厂方法1,创建一个定长的线程池,可控制线程最大并发数,超出的线程会在队列中等待
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>());
    }
    public static void main(String[] args) {
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            final int index = i;
            fixedThreadPool.execute(new Runnable() {
                public void run() {
                    try {
                        System.out.println(index);
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        fixedThreadPool.shutdown();
    }
}
  1. 工厂方法2

public class FactoryTest2 {
    // 工厂方法2,创建一个可缓存的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,否则新建
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
    }
    public static void main(String[] args) {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            try {
                Thread.sleep(index * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            cachedThreadPool.execute(new Runnable() {
                public void run() {
                    System.out.println(index);
                }
            });
            int poolSize = ((ThreadPoolExecutor) cachedThreadPool).getPoolSize();
            System.out.println("线程池中当前线程的数量: " + poolSize);
        }
        cachedThreadPool.shutdown();
    }
}
  1. 工厂方法3(用不上,只有一个线程,就是去了线程池的作用和意义了)

public class FactoryTest3 {
    // 工厂方法3,创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService(
                new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()));
    }
    public static void main(String[] args) {
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            singleThreadExecutor.execute(new Runnable() {
                public void run() {
                    try {
                        System.out.println(index);
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        singleThreadExecutor.shutdown();
    }
}
  1. 工厂方法4

public class FactoryTest4 {
    // 工厂方法4,创建一个定长线程池,支持定时及周期性任务执行
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    public static void main(String[] args) {
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
        scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
            public void run() {
                System.out.println("延迟10秒后执行,之后再按照每3秒执行一次");
            }
        }, 10, 3, TimeUnit.SECONDS);
    }
}

27.4.5、实现Runnable接口比继承Thread类所具有的优势

  1. 可以把相同的一个执行任务(Runnable接口的实现),交给不同的线程对象去执行

  2. 可以避免Java中的单继承的局限性。

  3. 线程和执行代码各自独立,实现代码解耦

27.5、线程的生命周期、线程状态

  • 新建状态:new线程对象没有调用start()之前

  • 就绪状态:线程调用start()方法,线程准备就绪,只等待CPU的调度就绪状态的线程就会进入线程池,CPU的调度就是在线程池中完成

  • 运行状态:CPU正在执行的线程,正在执行run方法

  • 死亡状态:当run方法执行完,则进入死亡状态

  • 阻塞状态:睡眠状态、加锁状态、等待状态,阻塞状态下的线程会接受某些条件之后就会回到就绪状态

27.6、线程的方法

27.6.1、sleep(long millisecond)

  • 无论是继承Thread还是实现Runnable都可以直接使用,让当前运行的线程睡眠指定时间,需要环绕try-catch

  • 静态方法,可用Thread直接调用

  • 若sleep中被interrupt,那就会触发InterruptedException

  • 只是释放CPU,不会释放锁

Thread.sleep(毫秒数)

27.6.2、setDaemon(true)

将线程设置成后台线程

27.6.3、getName()

  • 继承Thread就直接getName(),

  • 实现Runnable需要Thread.currentThread.getName()

27.6.4、getPriority()、setPriority(Thread.MAX_PRIORITY/数字)

获取储线程优先级,设置线程优先级,设置优先级的代码的底层是native方法,不是Java语言实现的,优先级也可以写1-10的数字,大的高

27.6.5、getThreadGroup()

获取当前线程的线程组,是ThreadGroup对象

27.6.6、group.activeCount()

获取线程组中存活的线程个数

27.6.7、group.enumerate(array)

将存活的线程存放到指定数组中,并返回存放的线程的个数

Thread[] array = new Thread[group.activeCount()];
//将存活的线程集中存放到指定数组中,并返回本次存放到数组的存活线程个数
System.out.println("arr数组中存放的线程个数为:"+group.enumerate(array ));

27.6.8、join()

  • 在线程2中写线程1.join(),就是让线程2等线程1结束才可运行,有时间参数的join方法就是进入Timed_Waiting状态

  • 必须环绕try-catch

  • 可以在线程1中加入线程2.interrupt(),来打断等待

  • 可以自己.join(),就是死锁

27.6.9、interrupt()

  • 底层是interrupt0,是native方法

  • 一般抛出InterruptedException这个异常,都是被打断了

  • interrupt 方法是通过改变线程对象中的一个标识的值(true|false),来达到打断阻塞状态的效果

  • 一个线程在阻塞状态下,会时刻监测这个标识的值是不是true,如果一旦发现这个值变为true,那么就抛出异常结束阻塞状态,并再把这个值改为false

27.6.10、isInterrupted()

  • 返回标识位状态,但不会重置标识位,即不会将true返回后变成false

27.6.11、interrupted()

  • 静态方法,可用直接通过Thread调用

  • 返回标识位状态,并且重置标识位,即将true返回后变成false

27.6.12、currentThread()

  • 静态方法,可用通过Thread直接调用,native方法

  • 获取当前线程

27.6.13、getState()

获取线程的状态

27.6.14、nextThreadNum()

获取当前运行线程的线程名,即Thread-后面的那个数字,从0开始

27.6.15、wait()

  • 必须在synchronized中才能使用,对象.wait()

  • 既释放CPU,也释放锁

  • 也可以设置时间,一般不用

27.6.16、notify()

  • 必须在synchronized中才能使用,对象.notify()

  • 随机将一个在等待池中等待该对象锁的线程放入锁池,锁可用时其变成可运行状态

27.6.17、notifyAll()

  • 必须在synchronized中才能使用,对象.wait()

  • 将所有在等待池中等待该对象锁的线程变成放到锁池,锁可用时变成可运行状态

27.7、线程调度

  • 时间片,当前一个线程要使用CPU的时候,CPU会分配给这个线程一小段时间(毫秒级别),这段时间就叫做时间片,也就是该线程允许使用CPU运行的时间,在这个期间,线程拥有CPU的使用权。

  • 如果在一个时间片结束时,线程还在运行,那么这时候,该线程就需要停止运行,并交出CPU的使用权,然后等待下一个CPU时间片的分配

27.7.1、抢占式调度

  • 系统会让优先级高的线程优先使用 CPU(提高抢占到的概率),但是如果线程的优先级相同,那么会随机选择一个线程获取当前CPU的时间片。例如JVM中的线程,就是抢占式调度,main线程最先入栈

  • 注意:优先级高只是提高抢占的概率,并不是一定

27.7.2、时间片轮转调度

所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间

27.8、线程的分类

27.8.1、前台线程(执行线程、用户线程)

  • 这种线程专门用来执行用户编写的代码,地位比较高,JVM是否会停止运行,就是要看当前是否还有前台线程没有执行完,如果还剩下任意一个前台线程没有“死亡”,那么JVM就不能停止!

  • 例如,执行程序入口的主线程(main),就是一个前台线程,在单线程程序中,main方法执行完,就代表main线程执行完了,这时候JVM就停止了

27.8.2、后台线程(守护线程、精灵线程)

  • 这种线程是用来给前台线程服务的,给前台线程提供一个良好的运行环境,地位比较低,JVM是否停止运行,根本不关心后台线程的运行情况和状态。

  • 例如,垃圾回收器,其实就一个后台线程,它一直在背后默默的执行着负责垃圾回收的代码,为我们前台线程在执行用户代码的时候,提供一个良好的内存环境

27.8.3、代码中的线程默认是前台线程,将其设置成后台线程

在主线程中,创建出来的线程对象,默认就是前台线程,在它启动之前,我们还可以给它设置为后台线程

t1.setDaemon(true);

27.9、线程组

  • 创建的时候若没有指定线程组,就默认父线程是当前线程组,main

  • 只有创建对象的时候才指定线程组,线程运行中途不可改变其线程组

27.9.1、指定线程组

ThreadGroup group = new ThreadGroup("我的线程组");
Thread t1 =new Thread(group, "t1");
ThreadGroup myGroup = t1.getThreadGroup();

27.10、正常中断的方式

27.10.1、设置标志位

自己设置一个静态的标志位,如果达到你要的临界条件就跳出run()

27.10.2、stop()

已过时停用,不安全

27.11、线程安全

  • JVM内存中的堆区,是一个共享的区域,是所有线程都可以访问的内存空间。

  • JVM内存中的栈区,是线程的私有空间,每个线程都有自己的栈区,别的线程无法访问到自己栈区的数据

  • 线程安全可以简单理解为一个方法或者一个实例可以在多线程环境中使用而不会出现问题

27.11.1、线程同步

27.11.1.1、synchronized
  • 加对象锁,是同步互斥锁

  • synchronized(对象) {临界区,可能出现线程不安全的代码},临界区只会让一个线程执行,执行完后才会释放对象锁

  • ()内的对象必须是引用对象,不可以是八大基本类型,里面的对象也可以跟业务没有任何关系,还可以使用this,把当前类的引用对象作为锁

  • 加了synchronized关键字的线程称为互斥线程

  • 可用直接加在方法上,就是默认锁this

  • synchronized关键字修饰非静态方法,默认使用 this 当做锁对象,并且不能自己另外指定

  • synchronized关键字修饰静态方法,默认使用 当前类的Class对象 当做锁对象,并且不能自己另外指定

  • 太霸道了,一直拿着锁

27.11.1.2、wait()、notify()、notifyAll()
  1. 任何对象中都一定有这三个方法,因为在Object中

  2. 只有对象作为锁对象的时候,才可以调用,即该对象必须是锁对象

  3. 只有在同步的代码块中,才可以调用

  • 线程之间的通信——同步线程:线程间执行有顺序,线程间互相影响

  • 线程通信步骤:

  1. 找到同一个对象

  2. 使用wait()、notify()、notifyAll()方法

  3. 先等待,后通知,否则等待的那个就死锁了

27.11.1.3、使用类对象当锁

这里编译不报错,但运行报错,因为是使用Class当锁对象,而不是this,所以直接使用wait()方法会运行时出错

public void test()throws Exception{
  synchronized (getClass()) {
    for(int i=0;i<100;i++){
      if(i==10){
        wait();
      }
    }
  }
}

27.12、sleep()、wait()的共同点和区别

27.12.1、共同点

  1. 都可以使线程阻塞

  2. 都可以释放CPU

27.12.2、不同点

  1. wait()方法必须写在同步方法中,是为了防止死锁和永久等待,使线程更安全,而sleep()方法不需要有这个限制

  2. wait()方法调用后会释放锁,sleep()方法调用后不会释放锁。

  3. sleep()方法必须要指定时间参数;wait()方法可以指定时间参数也可以没有。

  4. 两个方法所属类不同,sleep()方法属于Thread类;wait()属于Object类中,放在Object类中是因为Java中每个类都可以是一把锁

27.13、使用JDK工具查看JVM当前运行状况

27.13.1、在cmd中输入jconsole

选择本地进程进行连接

27.13.2、使用 不安全的连接

28、IO流

IO有一个available()方法,可以获得此次流读取的字节数

  • 数据以二进制的形式在程序与设备之间流动传输,就像水在管道里流动一样,所以就把这种数据传输的方式称之为输入流、输出流。这里描述的设备,可以是文件、网络、内存等

  • 流一般是先关输出流,再关输入流

  • 好像现在的write方法中都自带了flush方法,所以不写也不要紧

  • 获取流之后只能读一次,要从头开始继续读就要再获取一次流

  • 只有缓冲流需要用到flush,其他都是直接进行读写

  • 字节是byte,是整数类型;字符是char,是字符类型,所以使用Arrays.toString、println前一个是输出数字,后一个是输出字符

  • 字节容易乱码,是因为汉字是两个字节,而字节流存的时候是一个字节一个字节存,所以相当于把汉字拆一般存了

  • 字符流使用文件字符流复制图片时,不会报错,但大部分的图片看不了 因为字符流必须是两个字节,假如图片是奇数个字节,那么图片就会出错

  • 获取流之后在你不知道是什么下标的数据的时候,只能进行一次读取,因为读取是有光标的,你读取后就往后面去了,还想再读一次前面的数据就需要再new一次或RandomAccessFile的话有seek来重定光标

  • 养成无论什么输出流后面都加上flush,但RandomAccessFile没有flush方法,所以它是用System.out.flush();

28.0、关于追加写入、写出中内容和格式的问题

  • 追加写入容易产生问题,因为假如你是用同一个out从开发工具写出到文件中,那没什么事,

  • 但是如果你使用两个及以上的out,追加写出到文件中,那有可能会有问题,

  • 因为每次out写出的时候除了写内容外,还会写一些固定格式,即你分别写了四个,那本来应该是四个内容,一个格式,现在变成了四个内容,四个格式

28.1、流

28.1.1、流的分类

28.1.1.1、根据数据流向分
  • 输入流:把数据从其他设备上读取到程序中的流

  • 输出流:把数据从程序中写出到其他设备上的流

28.1.1.2、根据数据类型分
  • 字节流:以字节(byte)为单位,读写数据的流,1字节=8位二进制 字节流是万能输入输出流,就是效率慢点

  • 字符流:以字符(char)为单位,读写数据的流,1字符=2字节 字符流虽然比字节流快,但是有限制,比如不能读取图片、音频、视频等

28.1.2、流的结构

28.1.2.1、所有流的4个抽象父类
  • InputStream:字节输入流类

  • OutputStream:字节输出流类

  • Reader:字符输入流类

  • Writer:字符输出流类

28.1.2.2、使用流的3个基础特点
  1. 分清输入还是输出

  2. 分清字节还是字符

  3. 高清流的目的地

28.1.3、使用流的基础4步骤

  1. 声明流

  2. 创建流

  3. 使用流

  4. 关闭流

28.2、字节流

28.2.1、字节输入流InputStream的三个核心read方法

  • abstract int read():每次读取一个字节,返回读取的字节的值

  • int read(byte buff[]):每次读取多个字节,并放到buff数组中,返回读取的字节个数

  • int read(byte buff[], int start, int length):每次读取多个字节,并放到buff数组中,返回读取的字节个数,可以指定开始的位置,以及读取的字节的个数

  • skip方法:跳过指定字节数,再开始读取数据

28.2.2、字节输出流OutputStream的三个核心write方法

  • abstract void write():每次写出一个字节值

  • void write(byte buff[]):每次将buff数组中的值全部写出

  • void write(byte buff[], int start, int length):每次将buff数组中的值,从指定的位置开始,输出指定个数的值

28.2.2.1、3参write比1参write的好处

假如我们要取的文件总长100,设置buff长30

那么write就会把数据分为4块,30,30,30,10

都从0下标开始写出

3参的write是每次将数据块从0开始,放到buff数组中,放len(即你read的字节个数)个,然后再输出,不会发生错误

而1参的write则是每次将数据块从0开始,放到buff数组中,放30个,然后再输出,如果数据不足30个,就会从上一块拿前面的去补上,这样就会产生错误

28.2.2.2、write和println的区别
  • write会把byte、int转换成对应字符,然后输出,Hello

  • println就是直接输出值,不会转换成对应字符,72, 101, 108, 108, 111

28.2.3、控制台字节流

  • 控制台无论是输入空格还是回车,都不会有流结尾标志-1,所以while不会结束

// 1.声明流
InputStream in = null;
OutputStream out = null;
​
// 2.创建流,这里使用System中已经创建好的流对象
in = System.in;
out = System.out;
​
// 3.使用流
int len = -1;
byte[] buff = new byte[3]; // 这个只是限制每一次读取的长度,但是会3个3个全部读完的
try {
  // 这里判断条件中就已经在读取了,并且控制台是没有结尾的,即没有-1,所以不会结束
  while ((len = in.read(buff)) != -1) { 
    out.write(buff, 0, len); // 输出但没有换行
    System.out.println();  //这个是让你发现是3个3个输出的
  }
  out.flush();  // 刷新缓冲区,强制所有数据输出掉,因为输出是先写到缓存中的
} catch (IOException e) {
            e.printStackTrace();
} finally {
  // 4.关闭流,一般先关输出,再关输入
  IOClose.byteIOClose(in, out);
}

28.2.4、字节数组流(ByteArrayInputStream、ByteArrayOutputStream)

  • 数组字节输入流的输入要配合文件字节输出流来使用

ByteArrayInputStream bais = null;
ByteArrayOutputStream baos = null;
​
//byte[] arr = "Hello".getBytes(); // 数据来源
//bai = new ByteArrayInputStream(arr);
bais = new ByteArrayInputStream("Hello".getBytes());
baos = new ByteArrayOutputStream();
​
int len = -1;
byte[] array = new byte[1024];
try {
  while ((len = bais.read(array)) != -1) {
    //所以一般都配合FileOutputStream来使用
    baos.write(array, 0, len); // 输出不到,是因为没有指定输出路径
  }
  bao.flush();
  byte[] byteArray = baos.toByteArray();
  System.out.println(Arrays.toString(byteArray));
} catch (IOException e) {
  e.printStackTrace();
}

28.2.5、字节管道流(PipedInputStream、PipedOutputStream)

  • 管道因为要对接同一块内存,所以要先写入,再输出

  • 管道来源和位置都是内存

  • 管道不可避免地会出现错误,因为当write写完后,read读完还想再读,就报错了

  • 会管道的read会自动有阻塞作用,等待write写入后才会读取

PipedInputStream pis = null;
PipedOutputStream pos = null;
​
pis = new PipedInputStream();
pos = new PipedOutputStream();
​
//管道是先写到管道里面,在从管道输出
try {
  pos.connect(pis); // 建立管道关系,也可以是pi.connect(po);
  
  Thread t1 = new WriteThread(pos);
  Thread t2 = new ReadThread(pis);
  t1.start();
  t2.start();
  t1.join();  //让main线程等t1、t2
  t2.join();
} catch (Exception e) {
  e.printStackTrace();
}
​
class WriteThread extends Thread {
    private OutputStream out;
    public WriteThread(OutputStream out) {
        this.out = out;
    }
    @Override
    public void run() {
        byte[] arr = "hello world chw".getBytes();
        for (int i = 0; i < arr.length; i++) {
            try {
                out.write(arr[i]);
                out.flush();
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
class ReadThread extends Thread {
    private InputStream in;
    public ReadThread(InputStream in) {
        this.in = in;
    }
    @Override
    public void run() {
        int data = -1;
        try {
            while ((data = in.read()) != -1) {
                System.out.write(data);
            }
            System.out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } 
    }
}

28.2.6、字节文件流(FileInputStream、FileOutputStream)

  • 使用文件来写入时,若文件不存在,会帮你创建文件

28.2.6.1、获取文件对象
//绝对路径
//两种方法都可以,加\是因为要转义\
String path = "D:\\JavaWorkPlace\\jd2107work\\src\\test\\test1.java";
String path = "D:/JavaWorkPlace/jd2107work/src/test/test1.java";
File file = new File(path);
File file = new File("D:/JavaWorkPlace/jd2107work/src/test/test1.java");
​
String parent = "D:/JavaWorkPlace/jd2107work/src";
String child = "test/test1.java";
File file = new File(parent, child);    //parent必须是目录
​
String parent = "D:/JavaWorkPlace/jd2107work/src";
File p = new File(parent);
String child = "test/test1.java";
File file = new File(p, child);
​
​
​
//相对路径
//这个相对路径所相对的位置,是将来运行代码是执行java命令的路径,
//即权限类名中的包不算目录,所以相对的地方就是放包的地方
File file = new File("src/test/test1.java");
28.2.6.2、文件对象.方法
file.createNewFile()  //创建文件,但不可创建目录
file.mkdir();  //创建目录,但不可创建多级目录,即若t1/t2/t3都没有,那你不可以创建
file.mkdirs();  //可创建多级目录
file.delete();  //删除文件或目录
​
file.exists();  //判断文件或目录是否存在
file.isDirectory();  //判断文件是否是目录文件
file.isFile();  //判断文件是否是普通文件
file.getAbsolutePath();  //获取文件的绝对路径
file.getName();  //获取创建file对象的时候所传的路径
file.length();  //获取文件的字节个数
​
String[] array1 = file.list();  //以String数组返回目录中所以的子文件和子目录
//名字是简单名字,不会从src开始,不会递归
System.out.println(Arrays.toString(array1));
​
File[] array = file.listFiles();  //以File数组返回目录中所以的子文件和子目录
//名字是相对路径,会从src开始,不会递归
System.out.println(Arrays.toString(array));
​
//  查看指定文件的父级目录是否存在
if (!file.getParentFile().exists()) {
    file.getParentFile().mkdirs();  //得到父级目录路径,然后创建这个路径
}
28.2.6.3、使用
FileInputStream fis = null;
FileOutputStream fos = null;
​
try {
  //传的参数就是一个File或String
  fis = new FileInputStream("src/test/test1.java");
  fos = new FileOutputStream("src/test/test2.java");
  
  int len = -1;
  byte[] buf = new byte[40];
  while ((len = fis.read(buf)) != -1) {
    System.out.println(len);
    fos.write(buf, 0, len);
  }
  fos.flush();
  } catch (Exception e) {
    e.printStackTrace();
  } finally {
    IOClose.byteIOClose(fis, fos);
  }
}

28.2.7、字节数据流(DataInputStream、DataOutputStream)

  • 数据流是包装流,需要传一个字节流作为参数,只能是字节流

  • 指定数据类型(int、UTF、double,boolean)进行写入读出,因为假如你使用数组字节流写入,若数字较大的话取出来可能就乱了

  • 以什么顺序存进去的,就需要以那个顺序取出来,不然就乱了

DataOutputStream dos = null;
DataInputStream dis = null;
​
try {
  // 对于读写同一个文件,都是先写后读
  dos = new DataOutputStream(new FileOutputStream("src/test/test1.java"));
  dis = new DataInputStream(new FileInputStream("src/test/test1.java"));
  
  dos.writeInt(100);
  dos.writeBoolean(true);
  dos.writeDouble(10.5);
  dos.writeUTF("哈哈哈哈哈哈"); // 写字符串
  
  // 怎么存的顺序,就该怎么取
  int a = dis.readInt();
  boolean b = dis.readBoolean();
  double c = dis.readDouble();
  String d = dis.readUTF();
  System.out.println(a + "-" + b + "-" + c + "-" + d);
} catch (Exception e) {
  e.printStackTrace();
}

28.2.8、字节缓冲流(BufferedInputStream、BufferedOutputStream)

  • 缓冲流,可以在创建流对象时,设置一个默认大小的缓冲区数组,通过缓冲区进行读写,减少系统磁盘的IO次数,从而提高读写的效率

  • 缓冲流必须要使用flush方法

BufferedInputStream bis = null;
BufferedOutputStream bos = null;
​
try {
  bis = new BufferedInputStream(new FileInputStream(new File("src/test/test1.java")));
  bos = new BufferedOutputStream(new FileOutputStream("src/test/test2.java"));
  
  int len = -1;
  byte[] buf = new byte[1024];
  while ((len = bis.read(buf)) != -1) {
    bos.write(buf, 0, len); // 没写到文件中,只是写到缓存中了
  }
  bos.flush();// 缓存流必须刷新缓存,将缓存内容加载到文件中
} catch (Exception e) {
  e.printStackTrace();
} finally {
  IOClose.byteIOClose(bis, bos);
}

28.2.9、对象字节流(ObjectInputStream、ObjectOutputStream)

  • 对象流是包装流,只能包装字节流作其参数

  • 读写对象,那个对象的实体类必须序列化implements Serializable

  • 如果实体类中的某个变量不想参与序列化,相对于序列化透明,那么就要加上transient

  • 若对象是一个一个存的,那么取的时候要么你一个一个取,取固定数量,但一般都是循环取,那么就一定会碰到判断取的是否为空的时候,已经没有下一个了你还去取然后判断,就会报错

  • 所以都是存集合,取的时候取集合然后循环,就不会出错了

ObjectOutputStream oos = null;
ObjectInputStream ois = null;
​
try {
  oos = new ObjectOutputStream(new FileOutputStream("src/a.txt"));
  ois = new ObjectInputStream(new FileInputStream("src/a.txt"));
​
  Student student1 = new Student(1, "tom", 22, "男");
  Student student2 = new Student(2, "lily", 22, "女");
  
//  oos.writeObject(student1); // 专门用来存对象的
//  oos.writeObject(student2);
  List<Student> students = new ArrayList<Student>();
  students.add(student1);
  students.add(student2);
  oos.writeObject(students);
  
//  Student stu1 = (Student) ois.readObject();
//  System.out.println(stu1);
//  Student stu2 = (Student) ois.readObject();
//  System.out.println(stu2);
​
  ((List<Student>)ois.readObject()).forEach((s) -> System.out.println(s));
} catch (Exception e) {
  e.printStackTrace();
}

28.3、字符流

28.3.1、字符输入流Reader的三个核心read方法

  • int read():每次读取一个字符,返回字符的编码值

  • int read(char buff[]):每次读取多个字符,并放到buff数组中,返回读取的字符个数

  • int read(byte buff[], int start, int length):每次读取多个字符,并放到buff数组中,返回读取的字符个数,可以指定开始的位置,以及读取的字符的个数

  • skip方法:跳过指定字符数,再开始读取数据

28.3.2、字符输出流Writer的三个核心write方法

  • void write():每次写出一个字符值,字符可以用int表示

  • void write(byte buff[]):每次将buff数组中的值全部写出

  • void write(byte buff[], int start, int length):每次将buff数组中的值,从指定的位置开始,以及输出的字符的个数

  • void write(String str):每次写出一个字符串

  • void write(String str, int start, int length):写出一个字符串,字符可以用int表示,从指定的位置开始,以及输出的字符的个数

28.3.3、字符数组流(CharArrayReader、CharArrayWriter)

  • 数组字符输入流的输入要配合文件字符输出流来使用

CharArrayReader car = null;
CharArrayWriter caw = null;
​
char[] scource = "Hello 中国".toCharArray();
​
car = new CharArrayReader(scource);
caw = new CharArrayWriter(); // 本身也没有指定写到哪里
​
int len = -1;
char[] buff = new char[1024];
try {
  while ((len = car.read(buff)) != -1) {
    caw.write(buff, 0, len);
  }
  char[] charArray = caw.toCharArray();
  System.out.println(charArray);
} catch (IOException e) {
  e.printStackTrace();
} finally {
  IOClose.charIOClose(car, caw);
}

28.2.4、字符管道流(PipedReader、PipedWriter)

  • 管道因为要对接同一块内存,所以要先写入,再输出

  • 管道来源和位置都是内存

  • 管道不可避免地会出现错误,因为当write写完后,read读完还想再读,就报错了

  • 会管道的read会自动有阻塞作用,等待write写入后才会读取

PipedReader pr = null;
PipedWriter pw = null;
​
pr = new PipedReader();
pw = new PipedWriter();
​
//管道是先写到管道里面,在从管道输出
try {
  pw.connect(pr); // 建立管道关系,也可以反过来
  
  Thread t1 = new WriteThread(pw);
  Thread t2 = new ReadThread(pr);
  t1.start();
  t2.start();
  t1.join();  //让main线程等t1、t2
  t2.join();
} catch (Exception e) {
  e.printStackTrace();
}
​
class WriteThread extends Thread {
    private Writer out;
    public WriteThread(Writer out) {
        this.out = out;
    }
    @Override
    public void run() {
        char[] arr = "hello world chw".toCharArray();
        for (int i = 0; i < arr.length; i++) {
            try {
                out.write(arr[i]);
                out.flush();
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
class ReadThread extends Thread {
    private Reader in;
    public ReadThread(Reader in) {
        this.in = in;
    }
    @Override
    public void run() {
        int data = -1;
        try {
            while ((data = in.read()) != -1) {
                System.out.write(data);
            }
            System.out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } 
    }
}

28.2.5、字符文件流(FileReader、FileWriter)

  • 使用文件来写入时,若文件不存在,会帮你创建文件

FileReader fr = null;
FileWriter fw = null;
​
try {
  fr = new FileReader("src/AA.txt");
  fw = new FileWriter("src/BB.txt");
  
  int len = -1;
  char[] buff = new char[1024];
  while ((len = fr.read(buff)) != -1) {
    fw.write(buff, 0, len);
  }
} catch (FileNotFoundException e) {
  e.printStackTrace();
} catch (IOException e) {
  e.printStackTrace();
} finally {
  IOClose.charIOClose(fr, fw);
}

28.2.6、字符缓冲流(BufferedReader、BufferedWriter)

  • 缓冲流,可以在创建流对象时,设置一个默认大小的缓冲区数组,通过缓冲区进行读写,减少系统磁盘的IO次数,从而提高读写的效率

  • 缓冲流必须要使用flush方法

  • 字符缓冲输入流特有的方法:String line = bufferReader.readerLine();

BufferedReader br = null;
BufferedWriter bw = null;
​
try {
  br = new BufferedReader(new FileReader("src/AA.txt"));
  bw = new BufferedWriter(new FileWriter("src/BB.txt"));
  
//  int len = -1;  //传统方法
//  char[] buff = new char[1024];
//  while ((len = br.read(buff)) != -1) {
//    bw.write(buff, 0, len);
//  }
            
  String line = null;  //readLine()方法,提高效率
  // 只有BufferedReader才有读一行的方法
  while ((line = br.readLine()) != null) {
  bw.write(line);
  bw.newLine();
}
bw.flush();// 缓冲流必须刷新,放在里面没必要,降低效率
} catch (FileNotFoundException e) {
  e.printStackTrace();
} catch (IOException e) {
  e.printStackTrace();
} finally {
  IOClose.charIOClose(br, bw);
}

28.2.7、PrintStream、PrintWriter

这个Print我们经常用来打印输出,尤其是PrintWriter,用println()方法,打印并换行

28.4、转换流(InputStreamReader、OutputStreamWriter)

  • 可以指定编码格式

  • 转换流一般作用在网络传输中,传输过来的都是字节流,然后为了提高效率,将可以确定为文本之类的字节流转换成字符流,方便读写操作

  • 或用在转换字符编码上

  • 只有字节流转换成字符流,转换流并不是中转,而是单向转换的

  • 转换成字符输入流是为了使用readLine(),从字节输入流的数据拿来给字符输入流

  • 转换成字符输出流是为了配套字符输入流,将每行数据变成字节流出去,从字符输出流的数据拿出去给字节输出流

BufferedReader br = null;  //用来方便读写操作的缓冲流
BufferedWriter bw = null;
        
try {
  // 字节输入流,数据来源,模拟网络中字节流
  FileInputStream fis = new FileInputStream("src/AA.txt");
  // 将字节流转换成字符流,并指定编码格式,字节输入流成为其参数
  InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
  br = new BufferedReader(isr); // 我们要用的字符输入缓冲流,有readLine()
  
  //  和输入流一样的套路,就先拿个字节输出流
  FileOutputStream fos = new FileOutputStream("src/CC.txt");
  //  将字符流转换成字节流,指定编码格式,字节输出流成为其参数
  OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
  //  层层包装,把字符输出流的数据转换出去给字节输出流,防止乱码
  bw = new BufferedWriter(osw);
  
  //  先开始从字节往字符读,读后从字符往字节写出去
  String line = null;
  while ((line = br.readLine()) != null) {
    bw.write(line);
    bw.newLine();
  }
  bw.flush();
} catch (UnsupportedEncodingException e) {
  e.printStackTrace();
} catch (FileNotFoundException e) {
  e.printStackTrace();
} catch (IOException e) {
  e.printStackTrace();
}

28.5、随机访问流(RandomAccessFile)

  • RandomAccessFile没有继承那四个抽象父类(InputStream、OutputStream、Reader、Writer),而是继承了DataOutput, DataInput

  • 随机访问流既可以读取,也可以写入,所以当读写同一个文件的时候,只需要一个RandomAccessFile就好了,不然的话就需要两个,因为传参的文件路径是两个路径

  • 有seek(int position)方法,可以重定光标位置

  • write不止write,还有writeChars、writeBytes、writeDouble等

  1. "r"只读模式

  2. "rw"读写模式

  3. "rws"读写模式基础上,还要求对文件内容或元数据的每个更新都同步写入到底层设备

  4. "rwd"读写模式基础上,还要求对文件内容的每个更新都同步写入到底层设备

RandomAccessFile raf = null;
​
ByteArrayOutputStream baos = null;// baos用于临时保存一些内容
int replacePosition = 6;
String replaceContent = "chw";  //这是替换
int insertPosition = 6;
String insertContent = "World ";  //这是插入
​
try {
  raf = new RandomAccessFile("src/AA.txt", "rw");
  
  raf.seek(replacePosition);  //把光标定到替换位置
  raf.write(replaceContent.getBytes());  //写入,相当于替换
  
  int len = -1;
  byte[] buff = new byte[1024];
  raf.seek(0);  //把光标定到开头,从头开始读取
  while ((len = raf.read(buff)) != -1) {
    System.out.write(buff, 0, len); // 查看是否替换成功
  }
  
  //  插入其实就是先把后面的数据保存起来,写入后再把保存的数据写入
  baos = new ByteArrayOutputStream();  //用来保存插入位置后面的数据
  raf.seek(insertPosition);
  while ((len = raf.read(buff)) != -1) {
    baos.write(buff, 0, len); // 保存要插入的位置及后面的内容
  }
  System.out.println(Arrays.toString(baos.toByteArray()));
  
  raf.seek(insertPosition);  //定位到插入位置,来进行插入
  raf.write(insertContent.getBytes()); // 插入内容
  raf.write(baos.toByteArray()); // 继续把原来的内容加在后面
  
  raf.seek(0);
  while ((len = raf.read(buff)) != -1) {
    System.out.write(buff, 0, len); // 查看是否插入成功
  }
  System.out.flush();
} catch (FileNotFoundException e) {
  e.printStackTrace();
} catch (IOException e) {
  e.printStackTrace();
}

#

29、排序算法

29.1、冒泡排序

29.1.1、核心思想

  • 从前往后或从后往前,两个for循环,假如是前大后小,那么就是一步步判断前面的是不是比后面的小,小就换位置,一步步交换,

  • 第一遍 i 循环把小的放到最后面,然后第二遍把第二小的放倒二,一直继续

  • i 是长度-1,因为前面的都排好序了,那么最后一个一定是排好的

  • j 则是长度-1- i ,因为每一遍有 j 和 j+1 的判断,所以 j 一定不能取到最后一个,只能让 j+1取到,两两交换,我们都会把最大的放后面,那么就是 -1再减去已经放到后面的个数

29.1.2、代码

int[] array = { 1, 2, 3, 4, 5 };
​
for (int i = 0; i < array.length - 1; i++) {
  for (int j = 0; j < array.length - 1 - i; j++) {
    if (array[j] < array[j + 1]) {
      int temp = array[j];
      array[j] = array[j + 1];
      array[j + 1] = temp;
    }
  }
}
System.out.println(Arrays.toString(array));

29.2、选择排序

29.2.1、核心思想

  • 从前往后或从后往前,两个for循环,假如是前大后小,那么就是一轮轮找,每次找到一个最大值,

  • 记录最大值与 array[j] 去判断,若比max大的,就记录它的下标,假如 j 循环结束后记录的下标不等于 i ,那么就交换

  • i 是长度-1,因为前面的都排好序了,那么最后一个一定是排好的

  • j 则需要能取到最后一个数,因为没有 j+1 ,所以要能取到最后一个来判断,然后 j 的起始取值是 i+1,因为是后面的与前面的判断

29.2.2、代码

int[] array = { 1, 2, 3, 4, 5 };
​
for (int i = 0; i < array.length - 1; i++) {
  int key = i;
  int max = array[i];
  for (int j = i + 1; j < array.length; j++) {
    if (array[j] > max) {
      key = j;
    }
  }
  
  if (key != i) {
    int temp = array[i];
    array[i] = array[key];
    array[key] = temp;
  }
}
System.out.println(Arrays.toString(array));

29.3、插入排序

29.3.1、核心思想

  • 从前往后把数组分成两个,只是逻辑上分离,没有真的new两个数组,假如是要降序,那么从第一个开始,只要它比下一个大,那就从第二个开始判断,直到判断到比下一个小的,那么就找到了分离点了

  • 找到分离点后,将分离点后面的数组的数一个一个与分离点前面的判断

  • 先记录下那个位置和值,往前判断,假如比前面的数组的最后一个大,那就让那最后一个数往后移一个,然后刷新记录位置为当前位置,一直继续,直到比前面的小

  • 然后如果记录的位置不是 i ,那就把记录的值放到记录的位置上

29.3.2、代码

int[] array = { 1, 2, 3, 4, 5 };
int flag = 0;
​
for (int i = 0; i < array.length - 1; i++) {
  if (array[i] > array[i + 1]) {
    flag++;
  }
}
​
for (int i = flag + 1; i < array.length; i++) {
  int insertKey = i;
  int currentValue = array[i];
  
  for (int j = i - 1; j >= 0; j--) {
    if (currentValue > array[j]) {  
      array[j + 1] = array[j];
      insertKey = j;
    } else {
      break;
    }
  }
  
  if (insertKey != i) {
    array[insertKey] = currentValue;
  }
}
System.out.println(Arrays.toString(array));

30、网络编程

  • loaclhost就是127.0.0.1

30.1、软件结构

30.1.1、C/S(Client/Server、客户端/服务器)

  • 例如QQ、微信、网盘客户端等,只要是需要我们下载安装,并且和服务器通信的这一类软件,都属于C/S的软件结构

30.1.2、B/S(Browser/Server、浏览器/服务器)

  • 例如淘宝网、京东商城等,只要是需要使用浏览器,并且和服务器通信的这一类软件,都属于B/S的软件结构

30.1.2、C/S、B/S各有优势

  • C/S在图形的表现能力上以及运行的速度上肯定是强于B/S的

  • C/S/需要运行专门的客户端,并且它不能跨平台,用c++在windows下写的程序肯定是不能在linux下运行

  • B/S不需要专门的客户端,只要系统中安装了浏览器即可访问,方便用户的使用

  • B/S是基于网页语言的、与操作系统无关,所以跨平台也是它的优势

30.2、网络通信协议

30.2.1、TCP/IP协议

  • 传输控制协议/因特网互联协议( Transmission Control Protocol/Internet Protocol),它是一系列网络协议的总和,是构成网络通信的核心骨架。它是互联网中最基本、使用最广泛的协议。它定义 了计算机如何连入因特网,以及数据如何在计算机之间进行传输

  • TCP/IP协议栈采用4层结构,分别是应用层、传输层、网络层、链路层,并且在每一层的内部,都包含了一系列用于处理数据通信的协议,分别负责不同的通信功能

30.3、IP、端口号

30.3.1、IP

  • 互联网协议地址(Internet Protocol Address)。IP地址用来给一个网络中的计算机设备做唯一的编号

  • ipconfig:查看本机IP地址

  • ping IP地址:查看本机与另一台主机是否连通,在一个局域网中

30.3.2、端口号

  • 网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,每个需要网络通信的进程(应用程序),都会占用系统的一个端口号

30.3.2.1、Windows中查找指定端口号的进程PID并杀死
  • 需要管理员权限

netstat -aon | findstr 8888  //查看占用8888端口号的进程PID
TCP 0.0.0.0:8888 0.0.0.0:0 LISTENING 2696  //显示结果,找到占用8888端口号的PID
//强制关闭(杀死)
taskkill /F /pid 2696
30.3.2.2、Ubuntu中查找指定端口号的进程PID并杀死
netstat -anp | grep :8888  //查看占用8888端口号的进程PID
//显示结果,找到占用8888端口号的PID
tcp 0 0 :::8888 :::* LISTEN 3626/java
//强制关闭(杀死)
kill -9 3626

30.4、TCP、UDP

30.4.1、TCP(传输控制协议、Transmission Control Protocol)

  • TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立连接,然后再传输数据,它提供了两台计算机之间可靠的、无差错的数据传输

  • 三次握手

  1. 第一次握手,客户端向服务器端发出连接请求,等待服务器确认

  2. 第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求

  3. 第三次握手,客户端再次向服务器端发送确认信息,确认连接

30.4.2、UDP(用户数据报协议、User Datagram Protocol))

  • UDP是无连接通信协议,在数据传输时,数据的发送端和接收端不建立连接

  • UDP协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响

30.5、TCP、UDP的网络编程(套接字编程)

  • 网络编程中一定要flush()、close()

  • 读取网络字节流的时候可以包装成缓冲流

  • 服务器new ServerSocket(int port)后可以通过accept()来获取客户端对象,accept方法有阻塞作用

  • 客户端中有getInputStream()、getOutputStream()方法,来读取、写出字节流

  • 服务器先读取请求,再写回响应,客户端是先写出去请求,再读取响应

  • 通过线程来给每个客户端new一个线程去处理业务,然后在构造器里面一直循环等待客户端

30.5.1、TCP网络编程

  • java.net.ServerSocket 类表示服务端

  • java.net.Socket 类表示客户端

30.5.1.1、TCP网络编程步骤
  • 服务器端编程ServerSocket

  1. 创建ServerSocket对象,绑定端口号

  2. 等待客户端连入,若连入成功则返回一个Socket客户端对象

  3. 基于Socket对象获取输入流、输出流进行数据读写操作

  4. 关闭资源

  • 客户端编程Socket

  1. 创建Socket对象,并绑定IP和端口号

  2. 基于Socket对象获取输入流、输出流,进行数据读写操作

  3. 关闭资源

30.5.1.2、服务器
public class TCPServer2 {
    private ServerSocket serverSocket = null;
​
    public TCPServer2() {
        try {
            serverSocket = new ServerSocket(9999);
            System.out.println("服务器在9999端口启动");
            while (true) {
                Socket socket = serverSocket.accept();
                System.out.println("客户端来了");
                // 为每个客户端开辟一个线程
                new Thread() {
                    @Override
                    public void run() {
                        try {
//将网络中的字节流通过转换流编程缓冲流readLine()
                            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                            String msg = in.readLine();
                            System.out.println("客户端说:" + msg);
                            PrintWriter printWriter = new PrintWriter(socket.getOutputStream());
                            printWriter.println(new Date().toString());
                            printWriter.flush();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
​
    public static void main(String[] args) {
        new TCPServer2();
    }
}
30.5.1.3、客户端
Socket socket = null;
try {
    // 1、创建Socket对象
    socket = new Socket("127.0.0.1", 9999);
    // 2、读写操作
    PrintWriter printWriter = new PrintWriter(socket.getOutputStream());
    printWriter.println("给老子时间");
    printWriter.flush();
    
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    String msg = bufferedReader.readLine();
    System.out.println("服务器端说:" + msg);
} catch (UnknownHostException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

30.5.2、UDP网络编程

  • java.net.DatagramSocket:负责接收和发送数据

  • java.net.DatagramPacket:负责封装要发送的数据和接收到的数据

  • DatagramSocket的receive()方法有阻塞作用,会一直等到客户端发数据包过来

  • 服务器、客户端两边都需要 一个DatagramSocket,来receive和send然后需要两个DatagramPacket,一个发送数据包,另一个接收数据包

  • 服务器和客户端的操作正好相反

30.5.2.1、UDP网络编程步骤
  • 服务器端编程

  1. 调用DatagramSocket(int port)创建套接字,并绑定端口号

  2. 调用DatagramPacket(byte[] buf, int length)建立一个字节数组来接收客户端发送过来的UDP包

  3. 调用DatagramSocket的receive(DatagramPacket对象)接收客户端发过来的数据

  4. 创建数据包,用来存放给客户端响应回去的数据

  5. 向客户端发送数据send()

  6. 关闭资源

  • 客户端编程

  1. 调用DatagramSocket()创建套接字

  2. 创建数据包,用来存放给服务器的数据

  3. 向服务器发送数据send()

  4. 调用DatagramPacket(byte[] buf, int length)建立一个字节数组来接收服务器发过来的UDP包

  5. 调用DatagramSocket的receive(DatagramPacket对象)接收客户端发送过来的数据

  6. 关闭资源

30.5.2.2、服务器
try {
  // 1、调用DatagramSocket(int port)创建套接字,并绑定端口号
  DatagramSocket ds = new DatagramSocket(9999);
  
  // 2、调用DatagramPacket(byte[] buf, int length)建立一个字节数组
  //来接收客户端发送过来的UDP包,就等,等他发过来就装进去
  byte[] buf = new byte[1024];
  DatagramPacket dp = new DatagramPacket(buf, buf.length);
  
  // 3、调用DatagramSocket的receive(DatagramPacket对象)
  //接收客户端发过来的数据
  ds.receive(dp);
  System.out.println("客户端说:" + new String(dp.getData()));
  
  // 4、创建数据包,用来存放给客户端响应回去的数据
  byte[] buff = "你好客户端".getBytes();
  InetAddress address = dp.getAddress();
  int port = dp.getPort();
  DatagramPacket dp1 = new DatagramPacket(buff, buff.length, address, port);
  
  // 5、向客户端发送数据包send()
  ds.send(dp1);
  
  // 6、关闭资源
  ds.close();
} catch (SocketException e) {
  e.printStackTrace();
} catch (IOException e) {
  e.printStackTrace();
}
30.5.2.3、客户端
try {
  // 1、调用DatagramSocket()创建套接字
  DatagramSocket ds = new DatagramSocket();
  
  // 2、创建数据包,用来存放要发给服务器的数据
  //这个数据包专门用来发送数据
  String msg = "你好服务器";
  byte[] buf = msg.getBytes();
  InetAddress inetAddress = InetAddress.getByName("127.0.0.1");
  DatagramPacket dp = new DatagramPacket(buf, 0, buf.length, inetAddress, 9999);
  
  // 3、向服务器发送数据send()
  ds.send(dp);
  
  // 4、调用DatagramPacket(byte[] buf, int length)建立一个字节数组来接收服务器发过来的UDP包
  byte[] buff = new byte[1024];
  DatagramPacket dp1=new DatagramPacket(buff, buff.length);
            
  //5、调用DatagramSocket的receive(DatagramPacket对象)接收客户端发送过来的数据
  ds.receive(dp1);
  System.out.println("服务器说:" + new String(dp1.getData()));
            
  //6、关闭资源
  ds.close();
} catch (SocketException e) {
  e.printStackTrace();
} catch (IOException e) {
  e.printStackTrace();
}

30.6、URI、URL

  • URI(uniform resource identifier),统一资源标识符,用来唯一的标识一个资源。

  • URL(uniform resource locator),统一资源定位符,它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何定位这个资源

  • 例如,/hello ,这就是一个URI,它只是标识了一个资源

  • 例如,http://localhost:8888/hello,这就是一个URL,它不仅标识了一个资源,还能定位这个资源。

  • 就是URL对象有openConnection()方法来获取一个URLConnection对象,然后让此对象调用connect()来连接网络服务,后面就是正常的读取写入

try {
    // 1、创建url对象
    URL url = new URL("https://tse3-mm.cn.bing.net/th/id/OIP-C.73vDiH2zmCfW8iOyAlQGxwHaLH?w=204&h=306&c=7&r=0&o=5&dpr=1.25&pid=1.7");
    
    // 2、打开输入流
    URLConnection conn = url.openConnection();
    conn.connect(); // 连接网络服务器
    
    InputStream in = conn.getInputStream();
    FileOutputStream out = new FileOutputStream("src/b.jpeg");
    
    int len = -1;
    byte[] buff = new byte[1024];
    while ((len = in.read(buff)) != -1) {
        out.write(buff, 0, len);
    }
    out.flush();
} catch (MalformedURLException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

30.7、浏览器与服务器交互

  • 你获取读取流读一行,那一行就是网络数据信息,里面有GET/POST方式,访问路径,协议版本

  • 我们用split分开后可以来访问本地是否有此文件

  • 将本地 .html 文件写回去给浏览器的时候,要在获得输出流后加上这些话,才可以将 .html 文件中的内容进行编译写出

  • 并且要flush和close

OutputStream out = accpet.getOutputStream();
out.write("HTTP/1.1 200 OK\r\n".getBytes());
out.write("Content-Type:text/html\r\n".getBytes()); //这句话可以不要
out.write("\r\n".getBytes());
​
int len = -1;
byte[] buf = new byte[1024];
while ((len = fileInputStream.read(buf)) != -1) {
    out.write(buf, 0, len);
}
out.flush();
IOClose.byteIOClose(fileInputStream, out);

32、Java存储数据的几种方式

  • 基本数据类型

  • 引用数据类型

  • List

  • Map

  • File(文件)

33、System.getProperties

使用System.getProperties可以得到很多信息,

33.1、比较重要的几个Property

String basePath = System.getProperty("user.dir"); // 项目路径
String separator = System.getProperty("file.separator"); // 分隔符

34、String、StringBuffer、StringBuilder

  • String、StringBuffer、StringBuilder都实现了CharSequence接口,都用来封装字符串

  • StringBuffer、StringBuilder都继承了AbstractStringBuilder

  • 都实现了Serializable,都可以序列化

  • StringBuilder 和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)。

  • String是不可变的,StringBuffer、StringBuilder都是可变的

  • 字符串连接运算符 (+) 在内部使用 StringBuilder 类(可能不对)

34.1、String

  • String最大 2^16=65535,受限于javac限制,得 <=65534byte,最大内存占用4GB

  • String是不可变的,并且不可继承,因为final,即你创建出来对象后,这个对象的字符串就不可改变,因为已经放在字符串常量池里面了,

  • 因为String不可变,所以适合在多线程环境下使用

  • 所以如果字符串要经常改动,因为每次都会在字符串常量池创建,那么以前的就变成垃圾对象了,所以建议使用StringBuffer、StringBuilder

34.1.1、创建方式

  • 可以通过new对象的方式

  • 可以使用直接赋值,或通过+拼接

34.1.2、修改字符串的方式

  • 每次修改String,进行拼接异或是新new,都会在字符串常量池创建垃圾对象

34.1.3、是否重写了equals和hashCode方法

  • 重写了hashCode()方法和equals()方法

34.2、StringBuffer

  • 在使用 StringBuffer 类时,每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,所以如果需要对字符串进行修改推荐使用StringBuffer,但速度不如StringBuilder快

  • StringBuffer线程安全,加了 synchronized。因为 StringBuilder 的 append()方法调用的父类 AbstractStringBuilder 的 append()方法,里面 count 是已经使用的字符数组数量,然后 AbstractStringBuilder 的 append()方法 里面有一行代码 count+=len,这个是线程不安全的,那么就会出现 ArrayIndexOutOfBoundsException 异常。

  • Java1.0就有了

34.2.1、创建方式

  • 只可以通过new对象的方式

34.2.2、修改字符串的方式

  • 通过append的方法来进行追加,然后使用toString()方法变成String

  • 还有insert、delete、substring等方法进行修改

34.2.3、是否重写了equals和hashCode方法

  • 没有重写hashCode()方法和equals()方法,所以StringBuffer存入集合类可能会有问题

34.3、StringBuilder

  • 但由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。

  • Java1.5有的

34.3.1、创建方式

  • 只可以通过new对象的方式

34.3.2、修改字符串的方式

  • 通过append的方法来进行追加,然后使用toString()方法变成String

  • 还有insert、delete、substring等方法进行修改

34.3.3、是否重写了equals和hashCode方法

  • 没有重写hashCode()方法和equals()方法,所以StringBuffer存入集合类可能会有问题

35、关于字符串拼接

  • 使用"" + ""拼接字符串,这个操作其实在编译期就已经完成了,拼接完后把它放入字符串常量池

  • 而使用 str1 + str2拼接字符串,因为是变量,所以会在运行期间动态确认,这也就导致了他们是运行时拼接完成,然后放入字符串常量池

  • StringBuilder的效率是以下6种中最高的

35.1、字符串拼接的几种方式

35.1.1、+ 号拼接

  • 使用 + 操作符拼接,在编译的时候被替换成了 StringBuilder的append操作了

  • 也就是说 + 拼接,只是为了代码好看而已,实质用的是append方法

  • +=拼接是使用了StringBuilder的new然后再append,即chenmo += wanger 实际上相当于 (new StringBuilder(String.valueOf(chenmo))).append(wanger).toString()

35.1.2、StringBuilder的append拼接

  • append拼接的时候,首先,调用的其实是父类AbstractStringBuilder的append方法,所以我们就看父类的append方法

  • 先判断是不是null,如果是,当做字符串“null”来处理,调用appendNull()方法

  • 判断拼接后的字符数组长度是否超过当前值,如果超过,进行扩容并复制。使用ensureCapacityInternal 方法

  • 最后将拼好的str给value数组

35.1.3、StringBuffer的append拼接

  • 也是使用append进行拼接,但是加了个synchronized,保证线程安全

35.1.4、String的concat方法拼接

  • 如果要拼接的字符串的长度为0,那么就返回原来的字符串,否则就先将原字符数组放到buf数组中,

  • 现在的String改了很多,但原理还是把拼接的字符串 str 复制到字符数组 buf 中,并返回新的字符串对象getBytes(buf, 0, UTF16);str.getBytes(buf, len, UTF16);、return new String(buf, UTF16);

  • 但是如果拼接的是null,就会抛出空指针异常

35.1.5、String的join方法拼接

  • StringJoiner joiner = new StringJoiner(delimiter);

  • 所以底层其实用的是StringJoiner,返回的也是joiner.toString()

35.1.6、使用java.util.StringJoiner类的add方法拼接

  • 其底层使用了final来修饰我们给的,一定程度上保证了可见性,

  • final String elt = String.valueOf(newElement); #

36、抽象类和接口的区别

36.1、继承与实现

  • 类与类:extends,单继承

  • 类与接口:implements,多实现

  • 接口与接口:extends,多继承

36.2、区别

  1. 定义抽象类是abstract class,定义接口是interface

  2. 继承抽象类是extends,实现接口是implements

  3. 单继承,多实现

  4. 抽象类可以有构造方法,接口不可以有构造方法

  5. 抽象类中可以有成员变量,接口中只能有常量

  6. 抽象类可以有普通成员方法,接口中只能有抽象方法和default方法

  7. 抽象类中增加方法不影响子类;接口一般影响,除了default方法

  8. 抽象类是对事物的抽象,接口是对行为的抽象

  9. 接口更多的是在系统架构设计方法发挥作用,主要用于定义模块之间的通信契约。而抽象类在代码实现方面发挥作用,可以实现代码的重用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值