本笔记是跟着bilibili的"狂神说"的教程
学习做的笔记,感谢狂神无私分享。
Java 基础
预备知识
Java能干嘛
早年的塞班系统的手机游戏、桌面应用、Web应用软件开发、大数据Hadoop就是java写的。(C++是用来写桌面游戏的)
写博客和Markdown
写博客有非常多好处,可以提高自己的文笔组织能力。
Markdown 是程序员写博客必会的技能。推荐Typora、MarkText 这里两个软件。
Markdown 的语法自己网上学一下即可。
计算机软件
- 系统软件
- DOS(Disk Operating System)
- Windows
- Linux
- Unix
- Mac
- Android
- iOS
- 应用软件
- 英雄联盟
- …
DOS 命令
打开dos终端命令行窗口的方式之一: 在打开文件夹后上方的地址栏的地址最前面输入"cmd "回车即可
常用的 dos 命令:
- 切换盘符: 直接输入盘符,比如
c:
- 查看当前目录下所有文件:
dir
- 切换路径:
cd 路径
,如果当前路径和要切换的路径不是在一个盘符,需要使用/d
参数,比如F:\> cd /d E:\IDEA
- 清理屏幕:
cls
- 退出终端:
exit
- 查看电脑ip:
ipconfig
- ping 命令:
ping www.baidu.com
- 创建文件夹:
md <文件夹名称>
- 创建文件:
cd><文件名>
- 删除文件:
del <文件名>
- 删除文件夹:
rd <文件夹名称>
,注意,删除文件夹之前必须删除文件夹内的所有文件保证文件夹是空的才能删除
计算机语言的发展过程
- 第一代语言: 计算机语言
- 第二代语言: 汇编语言
- 应用: 软件逆向工程、机器人、病毒…
- 第三代语言: 高级语言,分为面向对象和面向过程2大类,比如 c 、 c++ 、 c# 、 java 、 python 等
Java 的发展过程
版本
- Java2标准版(J2SE): 占领桌面应用(不方便,败了),可用于桌面程序、控制台开发
- Java2移动版(J2ME): 占领手机(移动互联网还没兴起,败了,但后面Andoird时代来临又复兴了),可用于嵌入式开发,比如手机应用程序、小家电的系统
- Java2企业版(J2EE): 占领服务器(适合大项目,崛起)
成果
Java可以满足三高: 高可用、高性能、高并发
许多开发者和商业巨头基于Java开发了非常多的系统、工具、软件
- 构建工具:Ant,Maven,Jekins
- 应用服务器:Tomcat,Jetty,Jboss,Websphere,weblogic
- Web开发:Strus,Spring,Hibernate,myBatis
- 开发工具: Eclipse,Netbean,intellij idea,Jbuilder
优势
简单性、面向对象、可移植性(可跨平台)、高性能、分布式、动态性(反射)、多线程、安全性、健壮性(异常机制…)
Java 环境搭建
JDK JRE JVM
- JVM: Java 虚拟机
- JRE: Java 运行环境(Java Runtime Environmen)
- JDK: Java 开发工具包(Java Development Kit)
JDK包含JRE,而JRE包含JVM
JDK(Java Development Kit)是针对Java开发员的产品,是整个Java的核心,包括了Java运行环境JRE、Java工具和Java基础类库。
Java Runtime Environment(JRE)是运行JAVA程序所必须的环境的集合,包含JVM标准实现及Java核心类库。
JVM是Java Virtual Machine(Java虚拟机)的缩写,是整个java实现跨平台的最核心的部分,能够运行以Java语言写作的软件程序。
1、JDK
JDK是java开发工具包,在其安装目录下面有六个文件夹、一些描述文件、一个src压缩文件。bin、include、lib、 jre这四个文件夹起作用,demo、sample是一些例子。
可以看出来JDK包含JRE,而JRE包含JVM。
bin:最主要的是编译器(javac.exe)
include:java和JVM交互用的头文件
lib:类库
jre:java运行环境(注意:这里的bin、lib文件夹和jre里的bin、lib是不同的)
总的来说JDK是用于java程序的开发,而jre则是只能运行class而没有编译的功能。
JDK是提供给Java开发人员使用的,其中包含了java的开发工具,也包括了JRE。所以安装了JDK,就不用在单独安装JRE了。 其中的开发工具包括编译工具(javac.exe)打包工具(jar.exe)等
2、JRE
JRE是指java运行环境。光有JVM还不能成class的执行,因为在解释class的时候JVM需要调用解释所需要的类库lib。在JDK的安装目录里你可以找到jre目录,里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib和起来就称为jre。所以,在你写完java程序编译成.class之后,你可以把这个.class文件和jre一起打包发给朋友,这样你的朋友就可以运行你写程序了。
包括Java虚拟机(JVM Java Virtual Machine)和Java程序所需的核心类库等,
如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。
3、JVM
JVM就是我们常说的java虚拟机,它是整个java实现跨平台的最核心的部分,所有的java程序会首先被编译为.class的类文件,这种类文件可以在虚拟机上执行,也就是说class并不直接与机器的操作系统相对应,而是经过虚拟机间接与操作系统交互,由虚拟机将程序解释给本地系统执行。
可以理解为是一个虚拟出来的计算机,具备着计算机的基本运算方式,它主要负责将java程序生成的字节码文件解释成具体系统平台上的机器指令。让具体平台如window运行这些Java程序。
卸载 jdk
- 打开环境变量
- 找到
JAVA_HOME
的值对应的路径,删除该目录 - 删除环境变量
JAVA_HOME
、Path
中有%JAVA_HOME%
的环境变量 - 测试,cmd执行
java -version
,报错即成功
安装 jdk
- 官网下载类似于
java-8u181-windows-x64.exe
下载安装即可(需要同意协议才能下载),记住安装路径 - 安装完成后,配置系统环境变量
JAVA_HOME
为java的安装路径比如F:\xxxx\java\jdk1.8
Path
添加值:%JAVA_HOME%\bin
Path
添加值:%JAVA_HOME%\jre\bin
- 测试,cmd执行
java -version
,没报错即成功
Hello World
- 创建
HelloWorld.java
文件,内容如下
public class HelloWorld{
public static void main(String[] args){
System.out.println("Hello HelloWorld");
}
}
- 执行编译: cd 进入
HelloWorld.java
所在的目录,执行javac HelloWorld.java
,注意这里是javac
- 运行编写的编译后的java程序:
java HelloWorld
可能的报错:
- 关键字的英文大小写有误
- 文件名和类名不一致,且类名首字母必须大写
- 标点符号用了中文符号
基础语法学习
注释、标识符、关键字
- 注释
- 单行注释: 使用
\\
- 多行注释:
\*这里是多行注释*\
- 文档注释:
\** 回车,注释内容*\
- 单行注释: 使用
- 标识符: 类名、变量名、方法名等都被称为标识符,标识符必须以字母大小写、
$
、_
开始,标识符中可用有数字,且不能使用关键字作为标识符 - 关键字: class,new,public…
数据类型和变量
电脑中的数据单位
- 位(bit): 计算机内部存储的最小单位,只能是0或1
- 字节(B/byte):是计算机中数据处理的基本单位,1字节=8位,比如1字节可以存储的2进制是00000000-11111111,也就是02的8次方=0256
- 字符(bit):指的是计算机中使用的一个字母、数字、字或标点符号
Java中的数据类型
面试题
- 8大基本类型及取值范围(1字节byte/B=8位/bit,8位的二进制10101101最多可以表示256种状态,直接是计算机中数据处理的基本单位)
- 整型(byte、short、int、long)
- byte : 1字节,-128~127(-2的7次方到2的7次方-1)
- short : 2字节,-2的15次方到2的15次方-1
- int : 4字节,-2的31次方到2的31次方-1
- long : 要在值的后面加个"L"或"l",建议用大写,小写容易看错成1或大写的i,8字节,-2的63次方到2的63次方-1
- 浮点型(float、double)
- float:要在值的后面加个"F",4字节,3.402823e+38~1.401298e-45(e+38 表示乘以10的38次方,而e-45 表示乘以10的负45次方),如果要表示一个数据时float 型的,可以在数据后面加上 “F” 。
- double: 8字节,1.797693e+308~4.9000000e-324(同上)
- 字符型(char)
- char 可以赋值单个汉字、字母、数字:2字节,0~65536,对应Unicode(\u0000 - \uffff)
- 布尔型(boolean)
- 取值就两个:占1位,值为true或false
- 整型(byte、short、int、long)
- 引用类型
- 类
- 接口
- 数组
进制在Java中的使用
int i0 = 0b10; // 二进制: 首位+0b
int i1 = 010; // 八进制: 首位+0
int i2 = 10; // 十进制
int i3 = 0x10; // 十六进制: 首位+0x,0-9 A-F
浮点数的坑
面试题
float f = 0.1f; // 0
double d = 1.0/10; // 0.1
System.out.println(f==d); // 输出 false
float f1 = 397912738791827123f;
float f2 = f1 + 1;
System.out.println(f==d); // 输出 true
浮点数 float 和 double 的数据是有限、离散的、舍入误差、值只是大约的、接近但不等于
解决方法:
如果要比较浮点数或要求浮点数的计算不要有误差,就必须使用 BigDecimal 来比较
如果要比较浮点数或要求浮点数的计算不要有误差,就必须使用 BigDecimal 来比较
如果要比较浮点数或要求浮点数的计算不要有误差,就必须使用 BigDecimal 来比较
如果银行的业务也是使用的 BigDecimal。
字符扩展
面试题
java中的字符类型本质还是数字
char c1 = 'a';
char c2 = '中';
System.out.println(c1); // 输出 a
System.out.println(c2); // 输出 中
System.out.println((int)c1); // 强制类型转换后,输出 97
System.out.println((int)c2); // 强制类型转换后,输出 20013
// char 类型涉及到 Unicode 编码表,1个 char 占用2个字节,可以表示0~65535
char c3 = '\u0061'; // 表示 Unicode
System.out.println(c3); // 输出 a
String sa = new String("HelloWorld");
String sb = new String("HelloWorld");
System.out.println(sa==sb); // 输出 false
String sc = "HelloWorld";
String sd = "HelloWorld";
System.out.println(sc==sd); // 输出 true
类型转换
- 自动类型转换(子类型转父类型/低占用类型转高占用类型)
- 强制类型转换(父类型转子类型/高占用类型转低占用类型)
注意
- 不能对布尔值进行转换
- 不能把对象类型转换为不相干的类型
- 在把高占位类型数据转为底占位类型数据时,需要进行强制转换
- 转换时候可能存在内存移除,或者有精度问题
面试题
// 高占位类型 转 低占位类型 时,需要强制转换才行。比如 4 字节的 int 转 1 字节的 byte
int i = 128;
byte b = (byte)i; // 内存溢出了,byte 类型最大只能是 127,最小是-128
System.out.println(i); // 输出 128
System.out.println(b); // 输出 -128
// 低占位类型 转 高占位类型 时,可以自动转换。比如 4 字节的 int 转 8 字节的 double
int i1 = 128;
double d = i1;
System.out.println(i1); // 输出 128
System.out.println(d); // 输出 128.0
// 浮点数转换时的精度问题
System.out.println((int)23.7); // 输出 23
System.out.println((int)-234.89f); // 输出 -234
// int 也可以强制转为 char
// 内存溢出问题2
int money = 10_0000_0000; // JDK7的新特性,数字之间可以使用下划线分割
int years = 20;
int total = money*years; // -1474836480,因为计算的时候溢出了
long total2 = money*years; // 还是-1474836480,因为在赋值之前,在计算的时候还是 int 就已经溢出了
long total3 = ((long)money)*years; // 计算结果正确,计算时先把其中一个数先转为 long 就没有问题了
变量、常量、作用域
声明变量: 数据类型 变量名 = 值;
比如 String a = "测试的值";
也可以用逗号来隔开生命多个相同类型的变量,比如 int sum,ave;
或int a=1,b=2,c=3;
,但是**不建议这种做法,不建议在一行里定义多个值**。
- 局部变量: 方法中的变量,使用之前必须声明和初始化,不然报错
- 实例变量: 从属于对象,实例化后才能使用该变量,如果该变量没有初始化赋值时,值为该变量类型的默认值(数字类型的默认值是0或0.0,char是u0000,boolean是false,引用类型是null)
- 类变量: static
变量的修饰符不存在前后顺序,比如 static final double PI = 3.14;
和 final static double PI = 3.14;
是等价的
初始化变量的4种方式: 默认初始化、显式赋值初始化、调用类对象初始化和调用方法初始化
//Java 尽量保证所有变量在使用前都能得到恰当的初始化,否则会报异常
// 错误写法: 没初始化就使用变量
void f() {
int i;
i++;
}
//执行时会报异常,原因是变量i没有初始化
// 方法1: 默认初始化。只有实例变量才能使用这种方式,除了基本类型,其余类型的默认值都是 null
public class InitialValues {
boolean t;
char c;
byte b;
short s;
int i;
long l;
float f;
double d;
InitialValues reference;
void printInitialValues() {
System.out.println("Data type Initial value");
System.out.println("boolean " + t);
System.out.println("char[" + c + "]");
System.out.println("byte " + b);
System.out.println("short " + s);
System.out.println("int " + i);
System.out.println("long " + l);
System.out.println("float " + f);
System.out.println("double " + d);
System.out.println("reference " + reference);
}
public static void main(String[] args) {
new InitialValues().printInitialValues();
}
}
输出:
Data type Initial value
boolean false
char[] //char 的默认值为 0或者写为'\u0000',所以显示为空白
byte 0
short 0
int 0
long 0 //0L
float 0.0 //0.0F
double 0.0
reference null
// 方法2: 显式赋值
public class InitialValues2 {
boolean bool = true;
char ch = 'x';
byte b = 47;
short s = 0xff;
int i = 999;
long lng = 1;
float f = 3.14f;
double d = 3.14159;
}
// 方法3: 调用对象进行初始化赋值
class Depth {
}
public class Measurement {
Depth d = new Depth();
// ...
}
//注意:如果没有为 d 赋予初值就尝试使用它,就会出现运行时错误,会产生异常。
// 方法4: 调用方法进行初始化赋值
// 方法4.1:调用无参方法
public class MethodInit {
int i = f();
int f() {
return 11;
}
}
// 方法4.2:调用有参方法
public class MethodInit2 {
int i = f();
int j = g(i);
int f() {
return 11;
}
int g(int n) {
return n * 10;
}
}
//注意:调用有参方法时,方法中的参数不能是未初始化的类成员变量,否则会报异常
变量命名规范
- 所有变量、方法、类名:见名知意
- 类成员变量:首字母小写和驼峰原则,比如 “monthSalary”,除了第一个单词以外,后面的单词首字母大写,比如"lastName"
- 局部变量:首字母小写和驼峰原则
- 常量:大写字母和下划线,比如"MAX_VALUE"
- 类名:首字母大写和驼峰原则,比如"Man"、"GoodMan"方法名:首字母小写和驼峰原则:“run()”、“runRun()”
Java 的运算符
- 算术运算符:
+
,-
,*
,/
,%
,++
,--
。(因为普通的运算符都是需要两个数来进行运算,自增++
或自减--
只需要一个数,所以++
,--
也叫一元运算符) - 赋值运算符:
=
- 关系运算符(返回值是true/false):
>
,<
,>=
,<=
,!=
,instanceof
- 逻辑运算符(与、或、非):
&&
,||
,!
。&&
和||
是短路运算,如果&&
的第一个条件成立才会运算第二个条件,如果||
的第一个条件不成立才会运算第二个条件。 - 位运算符:
&
,|
,^
,~
,>>
,<<
,>>>
- 条件运算符(三目/三元运算符):
? :
,比如String type = score <60 ? "不及格" : "及格";
- 扩展赋值运算符:
+=
,-=
,*=
,/=
面试题
一些算术运算符特殊使用的例子
int a = 3;
int b = a++; // 先把值赋值给 b ,a 再自增 1 。 此行执行完后, b 是3, a 是4
System.out.println(a); // 输出 4
int c = ++a; // a 先自增,然后再把 a 赋值给 c ,所以此行执行完后 a是5,c也是5
System.out.println(a); // 输出 5
System.out.println(b); // 输出 3
System.out.println(c); // 输出 5
// 幂运算。很多运算我们都会用一些工具类来操作
int m = Math.pow(2,3); // 2的3次方
int a = 10;
int b = 20;
a+=b; // 这行等同于 a = a+b
a-=b; // 这行等同于 a = a-b
位运算符
/*
A = 0011 1100
B = 0000 1101
-----------------
位运算效率极高
A&B (与)结果: 0000 1100 // 两个数都为1时才得1
A|B (或)结果: 0011 1101 // 两个数只要有1就得1
A^B (异或)结果: 0011 0001 // 两个数相同则为0,否则为1
~B (取反)结果: 1111 0010 // 和原来二进制相反
<< 位左移,等同于原来的数乘以2
>> 位右移,等同于原来的数除以2
0000 0010
0000 0100 // 上面一行左移1位以后结果就是这行
0000 0010 // 上面一行右移1位以后结果就是这行
*/
Java 包机制
为了更好的组织类,Java 提供了包机制用于区别类名的命名空间,包的本质就是一个文件夹
格式: package pkg1[.pkg2[.pkg3...]]
,必须要写在每个 java 文件的第一行
导入包的时候就是import pkg1[.pkg2[.pkg3...]]
一般利用公司域名倒置作为包名。比如域名是blog.kuangstudy.com
,那么包名就定义为com.kuangstudy.blog
/*
比如 Domo01.java 在目录 com 的 baidu 的 blog 中,那么 Domo01.java 的第一行就得写上 `package com.badu.blog`
|--com
|--baidu
|--blog
|--Domo01.java
*/
package com.baidu.blog
// 导入包中的类
// import 包名全路径.类名
import java.util.Date
// 导入包中所有类
import com.baidu.blog.*
Java 文档
- @author: 作者名
- @version: 代码版本号
- @since: 指明需要最早使用的jdk版本
- @param: 参数名
- @return: 返回值情况
- @throws: 异常抛出情况
生成Java文档:
- 方法1: 手动生成文档,命令行
cd
进入代码所在目录后,执行javadoc -encoding UTF-8 -charset UTF-8 Xxx.java
,即可在当前目录生成一个index.html
,打开就是文档 - 方法2: IDEA 生成,具体操作查百度
Scanner 用户交互
java.util.Scanner 是 Java5 的新特性,我们可以通过该类获取用户输入。
Scanner s = new Scanner(System.in);
我们可以通过 Scanner
的 next()
和 nextLine()
获取输入的字符串,获取前一般都会通过 hasNext()
和 hasNextLine()
判断用户是否有输入。
Scanner s = new Scanner(System.in);
if (s.hasNext()){
System.out.println("用户输入的内容为: " + scanner.next());
// 凡是属于 IO 流的类,如果不关闭就会一直占用资源,要养成用完就 close 的好习惯
scanner.close();
}
next() 和 nextLine() 的区别
- next()
- 一定要读取到有效字符串之后才可以结束输入,也就是用户必须要输入,否则程序不停止。
- 对输入有效字符之前遇到的空白,
next()
方法会自动将其去掉。也就是输入字符串起始位置的空白字符都会被清空。比如输入" Hello",next()
得到的结果是"Hello" - 对输入有效字符中间的空白字符作为分隔符,比如输入"Hello World",第一次执行
next()
获取到的是空格前面的"Hello",第二次执行next()
才会获取到"World" - next()不能得到带有空格的字符串,所以要慎用
- nextLine()
- 以回车作为结束符,nextLine()方法返回的是回车之前的所有字符
- 可以获得空白符
Scanner的进阶用法
Scanner scanner = new Scanner(System.in);
System.out.println("请输入整数:");
if (scanner.hasNextInt()){
System.out.println("您输入的整数是:" + scanner.nextInt());
}else {
System.out.println("您输入的不是整数");
}
同理,有指定输入小数…等各种类型的数据。
流程控制
选择结构
- 顺序结构: 代码一行行从上到下执行
- if单选择结构: if
- if双选择结构: if else
- if多选择结构: if elif if
- 嵌套的if结构: if 里面还有 if
- switch多选择结构: switch中判断变量类型可以是 byte、 short、 int、char、String(Java7开始支持String),
switch(变量){case 值: 业务; break;}
,要学会在switch中使用 break 和 default,不用 break ,case 会有穿透现象。
反编译:将.class文件丢到IDEA即可查看.class反编译后的内容
循环结构
while
do...while
:do{}while()
for
:for(初始化;条件判断;迭代)
,初始化、条件判断、迭代都可以不写- 增强for循环(Java5新特性):
for(类型 变量: 数组对象){}
跳过本次循环: contitue
跳出循环/停止循环: break
面试题
练习
- 计算0-100之间奇数、偶数的和各是多少
- 用while或for循环输出1-1000之间能被5整除的数,并且每行输出3个(如果达到3个,就输出执行
println()
换行,否则使用print()
不换行输出) - 打印九九乘法表
- 打印101-150之间的所有质数
- 打印三角形
// 打印101-150之间的所有质数
// 质数: 指的是在大于1的自然数中,除了1和它本身,不能被其他自然数整除的自然数。即质数只能被1或它自己整除,比如3、5、7、11...
// java 中的 goto (在 java 中不叫 togo ,只是叫标签。 java 中没有 goto )。顶一个一个标签 outer ,然后跳到该标签
// 不建议使用下面代码的写法,了解即可
outer: for (int i=101; i<150;i++){
for (int j = 2; j<i/2; j++){ // j要小于i的倍数,就是如果j大于
if (i%j==0){ // 如果能被整除,说明不是质数,跳过
contitue outer;
}
}
System.out.print(i+" ");
}
// -----------------------------
// 打印星星三角形,
for (int i=1; i<=5; i++){
// 打印三角形对称轴左半边的空格
for (int j=5; j>=i; j--){
System.out.print(" ");
}
// 打印三角形对称轴左半边和中间的*
for (int j=1; j<=i; j++){
System.out.print("*");
}
// 打印三角形对称轴右半边的*
for (int j=1; j<i; j++){
System.out.print("*");
}
System.out.println();
}
// 最终效果:
// *
// ***
// *****
// *******
// *********
方法(函数)详解
方法的基础概念
方法的设计原则: 方法的本意就是功能块,我们设计方法的时候最好保持方法的原子性,原子性的意思就是一个方法只完成一个功能,这样更有利于我们后期的复用和扩展
方法结构
/*
修饰符 返回值类型 方法名(参数类型 参数名){
方法体: 这里写业务代码
return 返回值;
}
*/
// 实参和形参
public static int add(int a, int b){
// 定义方法时用来接收值的参数就叫形参,比如这里的 a 和 b 都是形参
return a+b;
}
add(1,3); // 调用方法时传入的值(参数),就叫实参,比如这里的1和3都是实参
方法的值传递和引用传递
// TODO 本章节待完成
重载和重写
面试题
- 重载: 在一个类中,有两个名称一样的函数,但是形参不同,需要达到以下条件
- 方法名必须相同
- 方法的签名必须不同(即参数个数不同,或参数类型不同,或参数排列顺序不同)都叫重载
- 方法的返回值类型可以相同也可以不同
- 仅仅返回值类型不同不是重载,这是被禁止的
- 重写: 子类重写父类的方法,覆盖了父类的方法
命令行传参
在调用java程序时传参和获取参数:
// Hello.java
package com.kuang.method
public static void main(String args[]){
for(String arg: args){
System.out.println(arg); // 输出 传入的参数
}
}
// 如果直接编译或执行报错: "错误: 找不到或无法加载主类Xxx",就使用以下步骤编译和执行即可。
// 编译: 进入 Hello.java 所在的目录,执行 `javac Hello.java`
// 执行: `java com.kuang.method.Hello`
// 执行: `java com.kuang.method.Hello this is kuangshen`
// args[] 就是 ["this", "is", "kuangshen"]
可变参数
jdk1.5开始,Java 支持传递同类型的可变长参数。
在方法声明中,在参数类型后紧跟着三个点"…"即可。
注意: 一个方法只能有一个可变参数,且可变参数必须是方法的最后一个参数。比如public void printMax(int a, double... numbers){}
public void test(int a, double... numbers){
// 接收到的 numbers 形参 是一个列表类型。可通过 numbers[0] 的形式访问
}
test(2,2.3,3.5); // 调用方式1: 可以直接传参
test(2,new double[]{2.3,3.5}); // 调用方式2: 可以用数组方式传参
递归
面试题 常问
递归就是一个方法中调用该方法自己,比如A方法中调用A方法。
递归结构包括2部分:
- 递归头: 就是什么时候不调用自身方法。如果没有头,将陷入死循环
- 递归体: 就是什么时候调用自身方法
// 递归 demo : 计算 n 的阶乘
public static int abc(int n){
if (n==1){ // 边界条件/递归头
return 1;
}else{
return n*abc(n-1);
}
}
递归的风险: 调用每个方法的时候,都会堆栈,此时会占用内存,一个方法调用完成时该方法才会出栈,如果一个方法递归比较深,也就是堆的栈比较多,会可能消耗大量内存,导致内存移除。所以能不用递归就不用,使用其他算法代替。
数组
数组是**相同类型数据的有序**集合。
数组的4个基本特点
- 数组长度是固定的,一旦数组被创建(创建是 new 的时候,不是声明数组变量的时候),它的大小不能改变了
- 数组中的元素必须是同一种类型
- 数组中的元素可以是任意数据类型,包括基本类型或引用类型
- 数组变量是应用类型,整个数组可以看成是对象,数组数组中的元素相当于该对象的成员变量。数组本身就是对象,Java 中的对象是在堆中存储的,数组变量所指向的对象也是在堆中。
数组的声明和创建
声明一个数组(的变量)
- 方法1(推荐):
int[] a;
- 方法2(不推荐):
int a[];
创建数组
使用 new 操作符来创建数组。格式为数据类型[] 变量名 = new 数据类型[数组长度]
,比如a = new int[10];
- 定义一个长度为5的数组(声明数组并创建数组):
int[] a = new int[5];
- 初始化数组元素:
int[] a = new int[]{1,4,5};
Java 的内存
- 堆: Java 中 new 出来的对象和数组对象都存在堆中,可以被所有线程共享,不会存放别的对象引用。也就是堆是存放具体的数据。
- 栈: 存放基本变量类型(包括基本类型的具体数值)或引用对象的变量(存放在堆里面的数据的引用地址)。也就是存放基本类型的数据或对象的引用。
- 方法区: 可以被所有线程共享,包含所有 class 和 static 变量
新建变量和赋值的流程在堆、栈中的体现
- 新建变量: 在栈中存放一个变量
- 给变量赋值: 在堆中存放值,并将第一步栈中的变量指向堆中的值
数组的三种初始化
- 静态初始化: 创建变量并赋值
int[] a = {1,2,3}; Student[] students = {new Student("张三",18),new Student("李四",19)};
- 动态初始化: 创建变量和赋值分开进行
int[] a = new int[2]; a[0] = 1; a[1] = 2;
- 默认初始化: 比如
int[] a = new int[10]
,此时只是指定了 int 类型的数组长度,没有给元素指定值,但是数组也会隐式初识化元素为 0
数组是引用类型,它的元素相当于实例变量,数组在堆中被分配空间时,每个元素也会按照实例变量同样的方法被隐式初始化。
数组的使用
- 获取数组长度:
数组对象.length
- foreach循环遍历(Java1.5=+(Java 1.5或以上版本)):
for(类型 变量 : 数组对象){}
- 面试题: 反转数组-创建临时数组对象,遍历原数组,第一个放到临时数组最后一个…
多维数组
// 二维数组
int[][] a = {{1,2},{2,2},{3,2}}
Arrays 类
数组的工具类 java.util.Arrays ,使用该类可以实现对数组的一些操作,比如排序、
Arrays 的使用
import java.util.Arrays
int[][] a = {1,2,3,7,2,0,8,1}
// toString() 输出数组
System.out.println(Arrays.toString(a)); // 输出: [1,2,3,7,2,0,8,1]
// sort() 对数组排序(无返回值,会改变原数组)
Arrays.sort(a);
System.out.println(Arrays.toString(a)); // 输出排序后的数组
// fill() 给数组赋值
Arrays.fill(a, 0); // 给 a 数组所有元素替换为 0
// 重载方法: Arrays.fill(a, 开始下标, 结束下标, 值);
// equals() 笔记两个数组中的元素的值是否相等
// binarySearch() 对排序好的数组进行二分查找法操作
排序算法
面试题
共有8大排序算法
- 冒泡排序,时间复杂度为O(n2),必须要会: 比较相邻两个元素,如果第一个数比第二个数大,就交换他们的位置,把大的元素放后面,每一次比较,都会产生一个最大或最小的数字。下一轮可以少一次排序(因为上次已经排序完一个元素了),一次循环直到结束。
- 选择排序
- 插入排序
- 快速排序
- 归并排序
- 希尔排序
- 堆排序
- 基数排序
稀疏数组
稀疏数组,是一种数据结构,比如一个数组中有大量相同的元素,将它设置为默认值,那么只需要将其他值作为有效值记录即可,其他的不是有效值就是默认值。
- 当一个数组中大部分元素为0,或者为同一值的数组时,可以使用稀疏数组来保存该数组。
- 稀疏数组的处理方式是:记录数组一共有几行几列,有多少个不同值;把具有不同值的元素和行列及值记录在一个小规模的数组中,从而缩小程序的规模
- 如下图:左边是原始数组,右边是稀疏数组,稀疏数组的第一行是存的元素数组的行、列数和有效数据的个数。之后的数据是记录每个有效数据的行、列坐标和数据的值
代码实现
public class SparseArray {
public static void main(String[] args) {
//创建一个二维数组 11*11 0:没有棋子, 1:黑棋, 2:白棋
int [][] a = new int[11][11];
a[1][2] = 1;
a[2][3] = 2;
System.out.println("原数组为:");
for (int[] x : a) {
for (int i : x) {
System.out.print(i + "\t");
}
System.out.println();
}
System.out.println("=============================");
//转化为稀疏数组
//先获取有效值的个数
int sum = 0;
for (int i = 0; i < 11; i++) {
for (int j = 0; j < 11; j++) {
if (a[i][j] != 0){
sum ++;
}
}
}
System.out.println("有效值的个数为:" + sum);
//创建稀疏数组
int[][] b = new int[sum+1][3];
b[0][0] = 11;
b[0][1] = 11;
b[0][2] = sum;
//遍历二维数组,将非0的值存放到稀疏数组
int count = 0;
for (int i = 0; i < a.length; i++) {
for (int j = 0; j < a[i].length; j++) {
if (a[i][j] != 0){
count++;
b[count][0] = i;
b[count][1] = j;
b[count][2] = a[i][j];
}
}
}
System.out.println("稀疏数组为:");
for (int i = 0; i < b.length; i++) {
System.out.println(b[i][0] + "\t" + b[i][1] + "\t" + b[i][2] + "\t");
}
System.out.println("=============================");
System.out.println("还原数组:");
//读取稀疏数组
int[][] c = new int[b[0][0]][b[0][1]];
//还原数组的值
for (int i = 1; i <b.length ; i++) {
c[b[i][0]][b[i][1]] = b[i][2];
}
System.out.println("还原数组为:");
for (int[] i: c) {
for (int j:i) {
System.out.print(j + "\t");
}
System.out.println();
}
}
}
执行结果
面向对象
面向对象和面向过程
- 面向过程: 第一步做什么、第二步做什么。比较适合处理一些简单的问题
- 面向对象: 分类的思维模式,将问题拆分成类别,适合多人协作。比如一个商城系统可以拆分成订单、配送、商品等类别,不同的人或代码去处理对应的工作
对于复杂的事物,我们从整体上合理的分析,使用的是面向对象的思路。在具体的细节方面,使用面向过程的方式去处理。
面向对象编程: Object-Oriented Programming,也就是OOP
面向对象编程的本质是: 以类的方式组织代码,以对象的方式组织(封装)数据
方法
方法的定义
- 修饰符
- 返回类型
- break 、 return 的作用和区别
- 方法名
- 参数
- 异常抛出
方法的调用
- 非静态方法: 必须实例化才能调用,实例化后非静态方法才存在,可以在非静态方法中调用静态方法
- 静态方法: 不用实例化就能调用,随类一起加载的,不能在静态方法中调用非静态方法
- 形参和实参
- 值传递和引用传递: 如果传参的数据类型是基本类型,则是值传递,如果是引用类型,则是引用传递,而引用也是地址(值),引用传递本质也是值传递,只不过传递的是内存地址,之后对该引用对象实际是在操作该对象。
- this 关键字
对象和类
- 类: 是一种抽象的数据类型,并不是某一个具体的事物。
- 对象是类的具体实例。比如张三(对象)是人(类)的一个具体的实例。
- 从代码运行角度来看,是先有类再有对象,由类作为模板来创建对象。
- 从认识论角度来看,是先有具体的对象,再抽取出类,比如先有苹果、桃子、香蕉,再抽取出 水果 这个分类。
对象的创建分析
创建对象使用 new 关键字。创建对象时,除了分配内存空间外,还会对该对象进行默认的初始化,比如调用类中的构造器(方法)。类中只有2个东西: 静态的属性(属性)、动态的行为(方法)
构造器
构造器特点:
- 构造器也称为构造方法,且它在 new 对象时肯定会被调用,new 关键字的本质就是在调用构造器。
- 构造器(构造方法)必须和类名相同
- 构造器没有返回值类型,也不能用 void 修饰
- 一个类中,即使我们没有给它写任何方法,但是编译后发现它会有一个空参数的构造器。也就是说就算我们一个类中没有定义方法,它还是会有一个方法(默认的构造方法)
- 有参构造器: 如果定义了有参构造器,那么就必须要显式地定义一个无参构造器(不用写方法体就行了)
- 构造器的作用就是初始化对象的值
Java 中的常量池
基本数据类型不会被存到常量池中。
Java中的常量池分为
- Class文件常量池: 主要存放字面量和符号引用
- 运行时常量池
- 全局字符串常量池: 存在于方法区
- 基本类型的包装对象常量池
对象创建在内存中的过程
面向对象的三大特性
面试题: 面向对象的三大特性是什么,封装、继承、多态分别又是什么意思?
- 封装
- 继承
- 多态
封装
我们程序设计要追求"高内聚、低耦合",高内聚就是类的内部数据操作细节由自己完成,不允许外部干涉,比如银行里限制每个人只能操作自己账户里的钱,不能操作别人账户;低耦合就是仅暴露少量的方法给外部使用。作用是提高程序安全性,保护数据,隐藏代码实现细节,统一接口,增加可维护性。用在类中就是,将属性私有,只能通过get、set方法访问属性,然后可以通过get、set校验属性,比如set年龄的时候可以规定不能超过150(岁)。
// 将属性封装起来,只能 通过 get 、 set 方法调用
package com.oop.demo04;
public class Student {
//封装一般针对属性
private String name;//名字
private int id;//学号
private char sex;//性别
private int age;
//提供一些可以操作这些属性的方法!
//提供一些public的get/set方法!
//get获得属性/数据
//set给属性/数据设置值
public String getName(){
return this.name;
}
public void setName(String name){
this.name = name;
}
//Alt + Insert快捷键,生成get/set
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public char getSex() {
return sex;
}
public void setSex(char sex) {
this.sex = sex;
}
public int getAge() {
return age;
}
public void setAge(int age) {
if (age>120 || age<0){
this.age = 3;
}else{
this.age = age;
}
}
}
// 调用
package com.oop;
import com.oop.demo04.Student;
public class Application {
public static void main(String[] args) {
Student s1= new Student();
Student s2= new Student();
String name = s1.getName();
s1.setName("秦疆");
System.out.println(s1.getName());
s1.setAge(125);
System.out.println(s1.getAge());
}
}
继承
使用 extends 修饰类,使得一个类继承另一个类,类和类之间的关系还有依赖、组合、聚合。比如"学生"类是"人"类的子类(也称派生类),子类和父类之间,具有"is a"的关系,比如 “Student is a Person”。在 Java 中,所有类都默认直接或间接继承 Object 。在 Java 中只有单继承,一个类只能继承一个类,而不能同时继承多个类,就像一个儿子只能有一个亲身爸爸,但是一个爸爸(父类)可以有多个儿子(子类)。注意: 私有的属性和方法无法被子类继承。
super 和 this 关键字
- super: super 代表父类对象的引用,可以使用 super. 调用的父类中的属性或方法。可以使用
super(这里也可以有参数)
调用父类构造器,如果显式调用父类构造方法,则super()
必须写在构造器的第一行 - this: this 代表调用所在的这个对象,可以使用 this. 调用本对象中的属性或方法。可以使用
this(这里也可以有参数)
调用本类构造器,如果显式调用本类构造方法this()
,则this()
必须 写在第一行 this()
或super()
必须在构造器的第一行,所以不能同时在构造器中调用this()
和super()
,会报错
权限修饰符
- public
- protected
- default
- private
// 父类/基类/超类: Person
package com.oop.demo05;
public class Person {
protected String name = "kuangshen";
public Person(){
System.out.println("Person 构造器被调用");
}
}
// 子类/派生类 Student : 子类继承父类,就会拥有父类的全部方法和属性,也就是 Student 继承了 Person 后,也会拥有 Person 类的属性和方法
public class Student extends Person {
private String name = "qinjiang";
public Student(){
// super(); 子类的构造器第一行会被隐式调用 super(); 来调用父类的构造器,如果要显式调用,必须要写在子类构造器的第一行,否则报错
System.out.println("Student 构造器被调用");
}
public void test(String name){
System.out.println(name);//输出: 秦疆
System.out.println(this.name); // 输出: qinjiang
System.out.println(super.name);// 输出: kuangshen
}
}
// 调用
package com.oop;
import com.oop.demo05.Person;
import com.oop.demo05.Student;
public class Application {
public static void main(String[] args) {
Person person = new Person();
Student student = new Student();
// 在创建 Student 类型的对象时,会先调用该类型的父类的构造器,再调用子类的无参构造器。也就是会输出"Person 构造器被调用",再输出"Student 构造器被调用"
student.test("秦疆");
}
}
多态
重写
重写:先有继承关系,子类重写父类的方法,重写的要求:
- 子类的方法名要和父类的方法名相同
- 子类的方法参数列表要和父类的方法参数列表相同
- 子类重写的方法的修饰符,范围可以扩大,但不能缩小: public > protected > default > private 。比如 父类的方法修饰符是 protecte ,那么子类重写该方法时 只能用 protecte 或 public 修饰
4、抛出的异常,范围可以缩小,但不能扩大: ClassNotFoundException(小)—>Execption(大)。比如 父类的方法抛出异常是 Execption ,那么子类重写该方法时只能抛出 Execption 或 ClassNotFoundException 、 IOException
重写:子类的方法和父类必须一致,方法体不同
为什么需要重写?
- 父类的功能子类不一定需要,或不一定满足
- 快捷键: Alt+Insert override
// 父类
package com.oop.demo05;
public class B {
public void test(){
System.out.println("B=>test()");
}
}
// 子类
package com.oop.demo05;
public class A extends B {
@Override//重写的注解,有功能的注释
public void test() {
System.out.println("A=>test()");
}
}
package com.oop;
import com.oop.demo05.A;
import com.oop.demo05.B;
public class Application {
// 重写静态方法和非静态方法的区别很大
// 静态方法:调用只和左边定义的数据类型有关,因为静态方法是属于类,而不属于对象,所以静态方法无法被继承,也就不存在重写这一说,调用变量的方法时调用的是变量的类型
// 非静态方法:只调用重写后的子类的方法
public static void main(String[] args) {
A a = new A();
a.test();// 输出 A=>test()
B b = new A(); // 父类的引用可以指向子类的对象
b.test();// 输出 B=>test()
}
}
多态
多态: 就是同一方法可以根据发送对象的不同而采用多种不同的行为方式,一个对象的实际类型在 new 的时候类型是确定的,但它所指向的变量的类型不一定是他自己,可能是它父类或爷爷类或子类
多态注意事项:
- 多态指的是方法的多态,属性没有多态
- 父类和子类,有联系才能转换
- 转换异常,ClassCastException
- 多态存在的必要条件
- 要有继承关系
- 方法需要重写
- 父类引用指向子类
以下几种修饰符修饰的方法不能重写,所以也就没有多态:
- static 修饰的方法是属于类,它不属于实例,所以也就没有静态方法被继承这一说,也就无法被重写
- final 是常量的修饰符,被 final 修饰的类时是不能被继承的(也就是被 final 修饰的类就断子绝孙了),被 final 修饰的方法也是禁止被重写的(但是可以被重载)
- private 方法,禁止被重写
// 父类
package com.oop.demo06;
public class Person {
public void run(){
System.out.println("run");
}
}
// 子类
package com.oop.demo06;
public class Student extends Person{
@Override
public void run() {
System.out.println("son");
}
public void eat(){
System.out.println("eat");
}
}
package com.oop;
import com.oop.demo06.Person;
import com.oop.demo06.Student;
public class Application {
public static void main(String[] args) {
Student s1 = new Student(); // 子类能调用自己的或继承自父类的全部方法和属性
Person s2 = new Student(); // 父类的引用指向了子类,该变量就只能调用父类的方法或属性
Object s3 = new Student(); // 父类的引用指向了子类,该变量就只能调用父类的方法或属性
// 对象能执行哪些方法,主要取决于左边的类型(即变量的类型),和右边(new 的对象的类型)关系不大
// 调用的方法如果没该方法就调用父类的方法,如果子类重写了该方法就调用子类的方法
s2.run();
s1.run();
((Student)s3).eat(); // 如果调用的变量类型是父类,但是想通过该变量调用子类的方法,就用强制转换转为子类即可
}
}
instanceof
instanceof 关键字是用来判断一个对象是否是某个类型,比如 if(a instanceof X){}
,取决与变量 a 所指向的对象和X是否有父子关系(直接继承或间接继承都算有父子关系),如果变量 a 所指向的对象和 X 有父子关系,那结果就是 true 。比如 B 类继承了 A 类,那么B的对象 instanceof A
和B的对象 instanceof B
结果都是 true,A的对象 instanceof A
和a的对象 instanceof B
结果也都是 true 。
如果一个对象 instanceof 一个类型结果为true,那么这个对象就可以通过自动或强制转换,转为该类型。
// 父类
package com.oop.demo06;
public class Person {
public void run(){
System.out.println("run");
}
}
// 子类1
package com.oop.demo06;
public class Student extends Person{
@Override
public void run() {
System.out.println("son");
}
public void eat(){
System.out.println("eat");
}
}
// 子类2
package com.oop.demo06;
public class Tercher extends Person{
}
package com.oop;
import com.oop.demo06.Person;
import com.oop.demo06.Student;
public class Application {
public static void main(String[] args) {
// 几个类的继承关系:
// Object > String
// Object > Person > Tercher
// Object > Person > Student
Object obj1 = new Student();
System.out.println(obj1 instanceof Student); // 输出: true
System.out.println(obj1 instanceof Person); // 输出: true
System.out.println(obj1 instanceof Object); // 输出: true
System.out.println(obj1 instanceof Tercher); // 输出: false
System.out.println(obj1 instanceof String); // 输出: false
Person obj2 = new Student();
System.out.println(obj2 instanceof Student); // 输出: true
System.out.println(obj2 instanceof Object); // 输出: true
System.out.println(obj2 instanceof Person); // 输出: true
System.out.println(obj2 instanceof Tercher); // 输出: false,变量 obj2 的类型 Person 和 Tercher 有父子关系,所以编译通过,但变量 obj2 所指向的对象 new Student() 没有父子关系,所以报错
// System.out.println(obj2 instanceof String); // 编译报错: 继承关系里需要连成一条线(在一行里)才不会报错,obj2 变量的 Person 类型和 String 类型不在同一条线里所以报错
Student obj3 = new Student();
System.out.println(obj3 instanceof Student); // 输出: true
System.out.println(obj3 instanceof Object); // 输出: true
System.out.println(obj3 instanceof Person); // 输出: true
// System.out.println(obj3 instanceof Tercher); // 编译报错
// System.out.println(obj3 instanceof String); // 编译报错
}
}
类型转换
- 父类引用指向子类的对象
- 子类转换为父类,向上转换,不需要强制转换,但会丢失掉子类有而父类没有的方法和属性
- 父类转换为子类,向下转换,需要强制转换
- 作用: 不用重新 new 一个对象来用,使得方法便于调用,减少重复的代码,更简洁
// 父类,没有 go 方法
package com.oop.demo06;
public class Person {
public void run(){
System.out.println("run");
}
}
// 子类,有 go 方法
package com.oop.demo06;
public class Student extends Person{
public void go(){
System.out.println("go");
}
}
package com.oop;
import com.oop.demo06.Person;
import com.oop.demo06.Student;
import com.oop.demo06.Teacher;
public class Application {
public static void main(String[] args) {
//类型之间的转换:父 | 子
//子类转换为父类,可能会丢失一些子类里的方法
//高 低
Person obj = new Student();
// obj.go(); // 此时会报错,因为 obj 的类型 Person 中没有 go 这个方法,go 方法在 Student 类型中才有,所以需要强制转换成 Student 才可以调用:
((Student) obj).go();
}
}
static 详解
被 static 修饰的方法叫静态方法,被 static 修饰的属性叫静态属性。调用静态属性或方法推荐用"类名."来调用。静态变量在内存中只有一个,静态变量在多线程中是共享的。
在静态方法中,只能调用静态变量和静态方法,而不能调用。因为静态变量和静态方法是在加载类时加载的,此时还没有成员变量和成员方法。
static 可以用来修饰以下东西
- 属性: 静态属性
- 方法: 静态方法
- 代码块: 静态代码块
- 导包: 静态导入包。比如平时使用随机数都是
Math.random()
,如果要单独导入 random() 这个方法,import 后面就要使用 static 来修饰导入的包,因为该方法是一个静态方法,导入后就可以直接使用random()
调用了
import static java.lang.Math.random
public class Person{
{ // 匿名代码块,可以用来给变量初始化值
System.out.println("匿名代码块");
}
static{ // 静态代码块,会在被在类加载时候执行,且只会被执行一次
System.out.println("静态代码块");
}
public Person(){
System.out.println("构造方法");
}
public static void main(String[] args){
Person person1 = new Person()
// 会输出静态代码块、匿名代码块、构造方法
Person person2 = new Person()
// 会输出匿名代码块、构造方法
}
}
抽象类和接口
普通类、抽象类、接口的特点:
- 普通类: 只有具体的实现
- 抽象类: 可以有具体实现的方法或规范(抽象方法)
- 接口: 只有规范
抽象类
abstract: 抽象的
- 被 abstract 修饰的类就是抽象类。
- 被 abstract 修饰的方法就是抽象方法
- 抽象方法没有方法体,如果继承了抽象方法所在的类,那么必须要在子类中重写该抽象方法
- 抽象方法所在的类也必须被 abstract 修饰,不然报错。也就是抽象方法只能在于抽象类中
- 抽象类不能被 new 出实例对象,抽象类的作用只是用来继承的。
思考: 抽象类不能被 new 对象,那么它存在构造器吗?抽象类存在的意义是什么?
接口(重点)
面试题
注意: 接口和抽象类都不可以被 new 实例化!接口和抽象类都不可以被 new 实例化!接口和抽象类都不可以被 new 实例化! new 接口 然后直接重写接口中的方法的这种写法,叫匿名内部类。面试时候可以说清楚
类是使用 class 来定义的,接口是使用 interface 来定义的。
接口用的最多,面向接口编程。在一个工程中,设计好了接口,具体实现找外包公司完成,很多外包都是做实现接口的工作,这样的工作进步不大,尽量别找这种工作。
接口的本质是精髓,面向对象的精髓是对象的抽象,最能体现对对象的抽象的就是接口。
如果只有一个方法的接口就叫函数式接口,可以使用 lambda 表达式简化,后面会有讲 lambda 表达式的章节。
接口的作用
- 约束
- 定义一些方法,让不同的人(开发人员写子类)实现该接口
- 接口的方法都是默认被
public abstract
修饰的 - 接口的属性都是默认被
public static final
修饰的 - 接口不能被实例化,接口中没有构造方法
- 一个类可以通过 implements 实现多个接口
- 实现了接口的子类必须要重写接口中的方法
// 接口都需要有实现类,才有意义
// 接口1
package com.oop.demo09;
public interface UserService {
//接口中定义的属性都是常量,即默认都是使用 public static final 来修饰的
int age = 99;
// public static final int age = 99; // 等同于上面一行的效果
//接口中定义的方法都是抽象的 public abstract(默认)
void add(String name);
void delete(String name);
void update(String name);
void query(String name);
}
// 接口2
package com.oop.demo09;
public interface TimeService {
void timer();
}
package com.oop.demo09;
//类通过 extends 只能继承一个抽象类,而通过 implements 可以实现多个接口,实现接口后需要重写接口中的方法
// 利用接口间接实现了多继承
public class UserServiceImp1 implements UserService,TimeService{
@Override
public void add(String name) {
}
@Override
public void delete(String name) {
}
@Override
public void update(String name) {
}
@Override
public void query(String name) {
}
@Override
public void timer() {
}
}
内部类及OOP实战
一个 .java
文件中可以有多个 类,但是 .java
文件中的最外层只能有一个 public class
成员内部类
package com.oop.demo10;
public class Outer {
private int id = 10;
public void out(){
System.out.println("这是外部类的方法");
}
public class Inner{
public void in(){
System.out.println("这是内部类的方法");
}
// 可以从内部类访问外部类的私有属性
public void getID(){
System.out.println(id);
}
}
}
package com.oop;
import com.oop.demo10.Outer;
public class Application {
public static void main(String[] args) {
Outer outer = new Outer();
// 通过外部类来实例化内部类,一般不用这种奇葩写法,了解即可
Outer.Inner inner = outer.new Inner();
inner.getID();
}
}
静态内部类
给一个内部类使用 static 修饰后就是静态内部类了,此时该内部类就不能访问外部的非静态成员变量了。
局部内部类
局部内部类: 就是定义在方法中的类。
package com.oop.demo10;
public class Outer {
//局部内部类
public void method(){
class Inner{
public void in(){}
}
}
}
匿名内部类(重点)
package com.oop.demo10;
public class Test {
public static void main(String[] args) {
//没有名字初始化类,不用将实例保存到变量中
new Apple().eat();
// 这个也叫匿名内部类
UserService userservice = new UserService(){
@Override
public void hello(){
}
};
}
}
class Apple{
public void eat(){
System.out.println("1");
}
}
interface UserService{//此处UserService不是接口,是接口的实现类
void hello();
}
异常处理
异常的分类
- 检查性异常: 比如用户要打开一个不存在的文件。
- 运行时异常: 可以被程序员避免的异常,编译时没报错,但是运行起来就报错了,比如递归调用栈溢出、除数为0,空指针,ClassNotFound,下标越界,UnKnowType…
- 错误ERROR: 错误不是异常,而是脱离了程序员控制的问题。
异常体系结构
java 把异常当作对象来处理,并定义一个基类java.lang.Throwable
作为所有异常的超类
在java API中已经定义了许多异常类,这些异常类分为两大类:错误 Error 和 Exception
Error
- Error类对象由Java虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关
- Java虚拟机运行错误(Virtual MachineError),当JVM不再有继续执行操作所需的内存资源时,将出OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止
- 还有发生在虚拟机试图执行应用时,如类定义错误(NoClassDefFoundError)、链接错误(LinkageError)。这些错误是不可查的,因为它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况
Exception
- 在Exception分支中有一个重要的子类RuntimeException(运行时异常)
- ArraylndexOutOfBoundsException(数组下标越界
- NullPointerException(空指针异常)
- ArithmeticException(算术异常)
- MissingResourceException(丢失资源)
- ClassNotFoundException(找不到类)等异常,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理
- 这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生
- Error和Exception的区别: Error通常是灾难性的致命的错误,是程序无法控制和处理的,当出现这些异常时,Java虚拟机(JVM)一般会选择终止线程
- Exception通常情况下是可以被程序处理的,并且在程序中应该尽可能的去处理这些异常
处理异常的关键字: try、catch、finally、throw、throws
捕获异常
package com.exception;
public class Test {
public static void main(String[] args) {
int a = 1;
int b = 0;
try { //try 包裹的代码块就是异常的监控区域
System.out.println(a / b);
} catch (ArithmeticException e) { //catch捕获异常
System.out.println("程序出现异常,变量b不能为0");
e.printStackTrace(); //输出错误的栈信息
} catch (Exception e) { //catch捕获异常,上面先捕获具体的异常,越到下面捕获的异常越大,也就是捕获多个异常时,要从小到大捕获
System.out.println("程序出现异常,变量b不能为0");
} finally { //finally 处理善后工作,不管有没有异常都会走到这里
System.out.println("finally");
}
}
}
/*
1、捕获异常必须得写 try、catch 这两个代码块,finally 非必要,假设需要关闭 IO 等操作可以写在 finally 中
2、catch 的内容是想要捕获的异常类型/级别/范围,可捕获多个异常,从上往下从小(细致)到大(宽泛),其中 throwable 是范围最大的集合
3、快捷键 Ctrl + Alt + T,可以选择try、catch、finally快速包裹需要检查的代码
*/
抛出异常
throws 是在定义方法时候用、
throw 是在方法体里面用
package com.exception;
public class Test2 {
public static void main(String[] args) {
try {
new Test2().test(1,0);
} catch (ArithmeticException e) {
e.printStackTrace();
} finally {
}
}
// 假设该方法内处理不了该异常,或不知道怎么处理,或需要调用方法的地方出来,就使用 throws 继续将异常抛出该方法之外
public void test(int a,int b) throws ArithmeticException{
if (b==0){
//throw 主动抛出异常,用在方法体中
throw new ArithmeticException();
}
}
}
自定义异常
- 定义一个异常类,继承 Exception 类
- 在方法体中通过 throw new 自定义异常类() 来抛出异常
//自定义的异常类
package com.exception.demo02;
public class MyException extends Exception {
private int detail;
public MyException(int a){
this.detail = a;
}
// 重写 toString() 自定义异常的打印信息
@Override
public String toString() {
return "MyException{" +
"detail=" + detail +
'}';
}
}
package com.exception.demo02;
public class Test {
// 存在异常的方法中如果不捕获该异常,就把异常抛出
static void test(int a) throws MyException {
System.out.println("传递的参数为:"+a);
if (a>10){
throw new MyException(a); // 抛出异常
}
System.out.println("OK");
}
public static void main(String[] args) {
try {
test(11);
} catch (MyException e) {
// 增加一些处理异常的代码块
System.out.println("MyException=>"+e);
}
}
}
到此 Java 基础结束。接下来就是 Java 进阶了
Java 常用类(需要常看这些类的源码看都有哪些用法)
Object
- hashCode()
- toString()
- clone()
- getClass()
- notify()
- wait()
- equals()
Math
常见的数学运算,比如求次方、开根号
Random
next…(): 生成随机数
UUID
File
- 创建文件
- 查看文件
- 修改文件
- 删除文件
基本类型的包装类
自动拆、装箱
Date
- Date(一般用来new Date(可能传入具体的时间))
- SimpleDateFormat
- Calendar(建议使用,比Date功能强大很多)
String
String 类是被 final 修饰的,具有不可变性。
经常用的是将byte流根据具体编码转成字符串的那个构造方法。
- isEmpty()
- length()
- charAt()
- getBytes()
- indexOf()
- substring()
- containts()
- replace()
- replaceAll()
- split(): 用的多
- toUpperCase()
- toLowerCase()
- trim()
面试题
// 写出以下三行代码的结果
String str = "a" + 1 + 2;
String str = 'a' + 1 + 2;
String str = 1 + 2 + "a";
StringBuffer(安全,推荐)
StringBuffer 是可变长字符串,线程安全,多线程或数据量大的时候使用,效率比 StringBuilder 低一点
StringBuilder
StringBuffer 是可变长字符串,线程不安全,单线程才用,效率比 StringBuffer 高
集合框架
面试题
Collection
List(有序可重复,常用)
- ArrayList(实现了 Iterator (迭代器)接口,且建议实现了迭代器接口的就使用迭代器进行遍历,注意: ArrayList 是多线程不安全的,比如多个线程同时给一个 ArrayList 添加元素,可能会导致前面添加的元素,被后面覆盖。比如2个线程各添加1000个元素,添加结束后可能 ArrayList 里面不足2000个元素)
- add()
- remove()
- contains()
- size()
- LinkedList(链表)
- getFrist()
- getLast()
- addFrist()
- addLast()
- removeFrist()
- removeLast()
- pop()
- push()
- Vector
- Stack(Vector 的子类)
Set(无序不重复)
- HashSet(常用)
- TreeSet
Map
面试题
- HashMap(超级重点,超级常用,面试第一高频问点)
HashMap 的数据结构: JDK7及之前是数组+链表,JDK8及之后是哈希表=数组+链表+红黑树。因为链表长了以后就变成像数组一样了,效率不高,如果长度超过8就转成红黑树。参考《[转载]Java-HashMap详解》 - TreeMap
Collection 工具类
泛型
泛型: 约束数据类型,避免类型转换出现问题
4种IO流详解
面试题
字节流(重点)
如果有中文,必须要用字符流,而不能用字节流
InputStream
OutputStream
字符流(重点)
如果有中文,必须要用字符流,而不能用字节流
Reader
Writer
处理流
Buffer
- BufferInputStream
- BufferOutputStream
- BufferReader
- BufferWriter
Data
- DataInputStream
- DataOutputStream
转换流
- InputStreamReader
- OutputStreamWriter
Filter
- FilterInputStream
- FilterOutputStream
- FilterReader
- FilterWriter
- PrintReader
- PrintStream
Object流
序列化与反序列化(重点)
如何序列化和反序列化
Serializable 接口
transient (透明的)关键字,如果不想对某个属性序列号,使用该关键字修饰该字段就不会被序列化了
Externalizable 接口,它是继承 Serializable 接口的,和 Serializable 类似
节点流/管道流
- CharArray开头的流
- CharArrayReader
- CharArrayWriter
- CharArrayInputStream
- CharArrayOutputStream
- String开头的流
- StringReader
- StringWriter
- Pipe开头的流(管道流)
- PipeInputStream
- PipeOutputStream
- File开头的流(文件流)(重点)
- 待完善
多线程(重点)
面试题
进程和线程
什么是进程、什么是线程?
多任务: 边吃饭/上厕所边玩手机。本质上大脑还是在同一个时间(某一个瞬间)只做了一件事
一个进程可以有多个线程,比如视频中可以有一个线程控制声音、一个线程控制图像、一个线程控制字幕,一个线程控制弹幕等等,所以可以同时看到这些东西
- 程序: 说起进程,就不得不说下程序。先看定义:程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
- 进程(Precess):而是一个静态的概念。而进程则是在处理机上的一次执行过程,它是一个动态的概念。
- 线程(Thread):通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程是 CPU 调度和执行的单位。
注意: 很多多线程都是模拟出来的,真正的多线程是指拥有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。
核心概念
- 线程就是独立的执行路径;
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如main线程和gc线程;
- main()称为主线程为系统的入口,用于执行整个程序;
- 在一个进程中如果开辟了多个线程,线程的运行由调度器安排调度,调度器与操作系统紧密相关的,先后顺序是不能人为干预的;
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;比如1万个人抢10张票,需要排队
- 线程会带来额外的开销,如cpu调度时间,并发控制开销,排队和调度时间也是需要时间的
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
线程的实现/创建的方式(重点)
继承 Tread 类(重点)
创建线程方式1
- 写一个类继承 Thread 类
- 在该类中重写 Thread 类的 run() 方法
- 创建这个类的对象
- 调用这个创建出来的对象的 start() 即可开启新的线程执行代码
注意: 如果是调用的是 run() 而不是 start() 不会开启新线程,只会当成普通的方法调用
public class TestThread1 extends Thread{
public void run(){
for (int i = 0; i < 20; i++) {
System.out.println("我正在看代码-->"+i);
}
}
//main线程
public static void main(String[] args) {
//创建线程对象
TestThread1 testThread1 = new TestThread1();
//调用start()方法开启线程
testThread1.start();
for (int i = 0; i < 200; i++) {
System.out.println("我正在看电视--》" + i);
}
}
}
注意: 执行 start() 之后,新的线程会开启但不一定立即执行,具体什么时候执行由 CPU 调度。结果会发现 main() 和 run() ,两条线程不是顺序执行的,是在很快的交替执行,并不是真正的并行。
练习案例: 使用 FileUtils 结合多线程,同时下载多个网图/文件
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
//下载器
class WebDownloader{
public void downloader(String url,String name){
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常");
}
}
}
// 通过使用静态变量和内部类实现参数传递,使用有参构造直接将url地址和下载的文件名传给downloader方法
public class TestThread2 extends Thread{
//练习Thread,实现多线程同步下载图片
private String url;//网络图片地址
private String name;//保存的文件名
public TestThread2(String url, String name) {
this.url = url;
this.name = name;
}
@Override
public void run() {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url,name);
System.out.println("下载了文件名为"+name);
}
public static void main(String[] args) {
TestThread2 t1 = new TestThread2("", "");
TestThread2 t2 = new TestThread2("", "");
TestThread2 t3 = new TestThread2("", "");
t1.start();
t2.start();
t3.start();
}
}
Tread 中有一个 start0() 方法,它是一个本地方法,java 无权调用,交给 c 语言调用
实现 Runnable 接口(重点,最重要)
创建线程方式2
- 首先定义一个类实现 Runnable 接口
- 在该类中实现 run() 方法,编写线程执行体,创建对象,调用 start() 方法。一般由于 Java 单继承的局限性都是使用 Runnable 对象
public class TestThred3 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("子线程输出--"+i);
}
}
public static void main(String[] args) {
//创建实现 Runnable 接口的类对象
TestThred3 testThred3 = new TestThred3();
//创建代理类对象并启动
new Thread(testThred3).start();
for (int i = 0; i < 50; i++) {
System.out.println("主线程输出--"+i);
}
}
}
通过查看 Thread 源码,发现 Thread 类也是实现了 Runnable 接口实现的多线程。
小结
- 继承Thread类(不推荐)
- 子类继承Thread类就可以具备多线程的能力
- 启动线程:子类对象.start()
- 不建议使用继承Thread类实现多线程,因为OOP单继承的局限性,如果继承了它,就不能继承别的类了
- 实现Runnable接口(推荐)
- 实现Runnable接口具有多线程能力
- 启动线程,传入目标对象+Thread对象.start()
- 推荐使用实现Runnable接口来实现多线程,避免单继承局限性,灵活方便,方便同一个对象被多个线程使用
函数式接口,可以使用lambda表达式使用
实现 Callable 接口(了解)
创建线程方式3
- 实现Callable接口,需要返回值类型作为Callable接口的泛型
- 重写call方法
- 创建目标对象
- 创建执行服务:
ExecutorService ser = Executors.newFixedThreadPool(1);
- 提交执行:
Future result1 = ser.submit(t1);
- 获取结果:
boolean r1 = result1.get();
- 关闭服务:
ser.shutdownNow();
callable的好处
- 可以定义返回值
- 可以抛出异常
利用callable改造下载图片案例
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;
public class TestCallable implements Callable<Boolean> {
private String url; //网络图片地址
private String name; //保存的文件名
public TestCallable(String url,String name){
this.url = url;
this.name = name;
}
//下载图片线程的执行体
@Override
public Boolean call() {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url,name);
System.out.println("下载了文件名为:"+name);
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable t1 = new TestCallable("https://timgsa.baidu.com/txxx1.jpg","1.jpg");
TestCallable t2 = new TestCallable("https://timgsa.baidu.com/txxx2.jpg","2.jpg");
TestCallable t3 = new TestCallable("https://timgsa.baidu.com/txxx3.jpg","3.jpg");
//1. 创建大小为3的线程池
ExecutorService ser = Executors.newFixedThreadPool(3); // 创建3个线程的线程池
//2. 将继承了 Callable 的对象提交给线程池执行
Future<Boolean> r1 = ser.submit(t1);
Future<Boolean> r2 = ser.submit(t2);
Future<Boolean> r3 = ser.submit(t3);
//3. 获取执行结果
boolean rs1 = r1.get();
boolean rs2 = r2.get();
boolean rs3 = r3.get();
System.out.println(rs1);
System.out.println(rs2);
System.out.println(rs3);
//4. 关闭服务
ser.shutdownNow();
// // 或者也可以用下面方式执行继承了 Callable 的对象的线程。效果等同于上面的代码
// FutureTask<Boolean> a1 = new FutureTask<Boolean>(new TestCallable("https://timgsa.baidu.com/txxx1.jpg","1.jpg"));
// FutureTask<Boolean> a2 = new FutureTask<Boolean>(new TestCallable("https://timgsa.baidu.com/txxx2.jpg","2.jpg"));
// FutureTask<Boolean> a3 = new FutureTask<Boolean>(new TestCallable("https://timgsa.baidu.com/txxx3.jpg","3.jpg"));
// new Thread(a1).start();
// new Thread(a2).start();
// new Thread(a3).start();
// 获取执行结果
// try{
// Boolean b1 = a1.get();
// Boolean b2 = a2.get();
// Boolean b3 = a3.get();
// }catch (InterruptedException e){
// e.printStackTrace();
// }catch (ExceptionException e){
// e.printStackTrace();
// }
}
}
//下载器 工具类
class WebDownloader {
//下载方法
public void downloader(String url,String name){
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,downloader方法出现问题");
}
}
}
并发问题的认识
多人买票的并发问题
public class TestThread4 implements Runnable{
private int ticketNum = 100;
@Override
public void run() {
while (true){
if(ticketNum<=0){
break;
}
// currentThread() 是用来获取当前线程,getname()是获取线程名
System.out.println(Thread.currentThread().getName()+"拿到了第"+ticketNum--+"张票");
}
}
public static void main(String[] args) {
TestThread4 testThread4 = new TestThread4();
// 小明,小红这些参数是给线程设置线程名
new Thread(testThread4,"小明").start();
new Thread(testThread4,"小红").start();
new Thread(testThread4,"小花").start();
new Thread(testThread4,"小丽").start();
}
}
最终发现执行代码时,有拿了重复的票的,比如小明和小红可能同时拿了第5张票。这就是多个线程操作同一个资源时,产生了线程不安全,数据发生了混乱
龟兔赛跑的并发问题
故事流程
- 首先来个赛道距离,然后要离终点越来越近
- 判断比赛是否结束
- 打印出冠军获得者
- 乌龟和兔子赛跑开始
- 龟兔赛跑的故事中讲述兔子需要睡觉,所以模拟兔子睡觉
- 最终乌龟赢得比赛
实现思路
- 首先需要知道跑道可以跑多少步,当前跑到了第几步,使用for循环实现
- 然后要有一个方法判断比赛是否结束,这里我们就封装了一个gameOver方法来判断,将兔子或者乌龟跑的步数传进该方法进行判断,为了严谨要先判断是否有胜者如果有的话退出该方法,没有接着判断
- 如果步数大于100如果大于说明已经到终点了使用Thread.currentThread().getName()将线程名字赋值给字段winner
- for循环里边有一个布尔值接收gameOver函数返回值,判断是否比赛结束。
- 使用实现Runnable接口的方式实现多线程
package com.m.demo04;
//模拟龟兔赛跑
public class Run_race implements Runnable{
// 冠军
private String champion;
// 子线程,重写run()方法
@Override
public void run() {
// 龟和兔子跑步的过程中显示的步数
for (int i = 0;i<= 100;i++ ){
// 模拟兔子在中途每50步就睡觉一次
if (Thread.currentThread().getName().equals("兔子") && i==50){
try {
// 假设兔子在中途每次睡觉都睡10毫秒
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
boolean flag = gameResult(i);
// 返回真值时,就跳出for循环
if(flag){
break;
}
System.out.println(Thread.currentThread().getName()+"跑了"+i+"步");
}
}
// 判断是否存在冠军
private boolean gameResult(int stepNum) {
// 冠军不为空时,返回true
if (champion != null) {
return true;
}else{
// 龟或者兔子跑的步数达到>=100时,最终成为冠军获得者
if (stepNum >= 100) {
champion = Thread.currentThread().getName();
System.out.println(champion + "是冠军获得者");
return true;
}
}
return false;
}
// 主线程
public static void main(String[] args) {
// 创建Runnable接口的实现类对象
Run_race race = new Run_race();
// 启动线程
new Thread(race,"兔子").start();
new Thread(race,"乌龟").start();
}
}
线程中的 run() 和 start()
run() 和 start() 的区别
静态代理和动态代理
静态代理(重点)
代理就是帮忙做一些事情的人或物或代码。比如你结婚的时候,婚庆公司可以帮忙处理一些你结婚的事情,婚庆公司此时就是你的代理。
package com.company.proxystatic;
// 主方法
public class StaticProxy {
public static void main(String[] args) {
new WeddingCompany(new You()).HappyMarry();
// new Thread(() -> System.out.println("结婚啦")).start(); // 和上面那行很像
}
}
// 创建接口,等待被用
interface Marry{
void HappyMarry();
}
// 真实角色,你去结婚。核心业务代码写这里
class You implements Marry{
@Override
public void HappyMarry() {
System.out.println("结婚了");
}
}
// 代理角色,帮助你结婚前后要做的一些事情
class WeddingCompany implements Marry{
private Marry target;
public WeddingCompany(Marry target){
this.target=target;
}
@Override
public void HappyMarry() {
marryBefore();
this.target.HappyMarry(); // 这就是真实对象原本要做的事
marryAfter();
}
private void marryAfter() {
System.out.println("婚后");
}
private void marryBefore() {
System.out.println("婚前");
}
}
静态代理总结:真实对象和代理对象都要实现同一个接口
静态代理好处:代理对象可以做很多真实对象做不了的事情,真实对象专注做自己的事情
比如: new Thread(Runnable).start()
这行代码就是用外层的 Tread 来代理内层的 Runnable,业务代码都写在 Runnable 中,但是执行是交给 Thread 去执行,所以 Thread 在这里的角色就是 Runnable 的代理。
动态代理
Lambda 表达式
Lambda 表达式的作用
- 避免匿名内部类定义过多不便阅读。
- 可以让代码看起来很简洁
- 去掉了一堆没有意义的代码,只留下核心逻辑
比如下面是使用 Lambda 表达式的Demo:new Tread(()->{业务代码}).start();
,里面应该是一个 Runnable 接口的实现类的,简化了之后就这样了,说明这 Runnable 接口里面只有一个 run 方法(查看源码发现也是如此)
Lambda 表达式的本质就是函数式编程,所以理解 Funcional interface (函数式接口) 是学习 Java8 lambda 表达式的关键所在
函数式接口的定义:如果一个只包含一个抽象方法,那么它就是一个函数式接口。即定义一个接口来当方法使用。比如public interface Runnable{public abstract void run();}
,对于函数式接口,我们可以通过 lambda 表达式创建该接口的对象
定义一个函数式接口的步骤
- 首先定义一个函数式接口(函数式接口就是接口中只有一个抽象方法)
- 实现接口,定义一个类实现接口对其方法进行重写
- 在main方法中创建一个接口对象执行方法(父类引用指向子类对象:
父类 变量(引用) = new 子类();
)
手动推导从普通方式到 Lambda 表达式: 从普通的调用方法,到内部类,到匿名内部类,到 lambda 表达式简化的写法
// 1. 调用代码的方式1: 原始的,普通调用方法的方式
interface ILike{
void lambda();
}
class Like implements ILike{
@Override
public void lambda() {
System.out.println("我爱 Java 普通调用");
}
}
public class TestLambda1 {
public static void main(String[] args) {
ILike like = new Like();
like.lambda();
}
}
// 2. 调用代码的方式2: 使用静态内部类
interface ILike{
void lambda();
}
public class TestLambda1 {
// 静态内部类: 将类写在另一个类中,并用 static 修饰
static class Like implements ILike{
@Override
public void lambda() {
System.out.println("我爱 Java 静态内部类调用");
}
}
public static void main(String[] args) {
ILike like = new Like();
like.lambda();
}
}
// 3. 调用代码的方式3:局部内部类
public class TestLambda1 {
public static void main(String[] args) {
// 局部内部类: 将类定义在方法中
class Like implements ILike{
@Override
public void lambda() {
System.out.println("我爱 Java 局部内部类调用");
}
}
ILike like = new Like();
like.lambda();
}
}
// 4. 调用代码方式4: 匿名该内部类(没有类的名称,必须借助接口或父类来实现),直接在代码中 new 接口并实现接口中的抽象方法
interface ILike{
void lambda();
}
public class TestLambda1 {
public static void main(String[] args) {
ILike like=new ILike() {
@Override
public void lambda() {
System.out.println("我爱 Java 匿名内部类调用");
}
};
like.lambda();
}
}
// 5. 调用代码方式5: 使用 lambda 表达式,类都不用定义了,直接重写接口实现即可。
interface ILike{
void lambda();
}
public class TestLambda1 {
public static void main(String[] args) {
ILike ike = () -> {
System.out.println("我爱 Java lambda 表达式调用");
};
// 如果接口的方法有参数,可以从下面简化掉括号,甚至简化掉大括号
// 如果有参数,简化前
// ILike ike = (a) -> {
// System.out.println("我爱 Java lambda 表达式调用"+a);
// };
// 简化1: 简化参数括号(如果是多个参数不能这么简化,会报错)
// ILike ike = a -> {
// System.out.println("我爱 Java lambda 表达式调用"+a);
// };
// 简化2: 简化大括号(只能有一行代码情况下才能像这里简化成一行)
// ILike ike = a -> System.out.println("我爱 Java lambda 表达式调用"+a);
ike.lambda();
}
}
线程的5种状态
新建状态
就绪状态
运行状态
阻塞状态
- sleep(时间)指定当前线程阻塞的毫秒数
- sleep存在异常InterruptedException
- sleep时间达到后线程进入就绪状态
- sleep可以模拟网络延时,倒计时等
- 每一个对象都有一个锁,sleep() 不会释放锁
// 案例: 使用 sleep() 使得线程阻塞,实现模拟倒计时10秒钟的效果
public class TestSleep {
public static void main(String[] args) {
timeDown();
}
public static void timeDown(){
int i=10;
while (true){
System.out.println("倒计时"+i--);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(i<=0){
break;
}
}
}
}
死亡状态
- 不建议使用 destor() 或 stop() 或 JDK 不建议使用的方法停止线程
- 建议让线程正常停止,比如利用循环n次就停止,不建议使用死循环
- 建议使用标志位停止,如下图
// 例子: 停止线程
import javafx.scene.paint.Stop;
public class TestStop implements Runnable{
//定义一个判断条件的标志位
private boolean flag=true;
public void run() {
int i =0;
while (flag){
System.out.println("landing..."+i++);
}
}
// 自定义一个公开的用于停止线程的方法
public void stop(){
this.flag=false;
}
public static void main(String[] args) {
TestStop testStop = new TestStop();
new Thread(testStop).start();
for (int i = 0; i < 100; i++) {
if(i==80){
//调用stop方法改变标志位
testStop.stop();
System.out.println("停止了");
}
System.out.println("此时运行了"+i);
}
}
}
线程常用方法
sleep()
join()
想象成一个新线程,插队到之前正在运行的线程中,且霸气的要先执行完它自己,其他线程(包括主线程)才能执行。
就是正常状态下,子线程和主线程由 CPU 随机调度,但是一旦有个有个子线程执行了 join() ,就先将主线程和其他线程阻塞,等该线程完之后才能执行主线程或其他线程,如果此时主线程正在执行,那么先停止主线程去执行子线程,子线程执行完之后主线程接着执行。
package Thread;
public class TestJoin implements Runnable{
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("我是vip"+i);
}
}
public static void main(String[] args) {
TestJoin tj = new TestJoin();
Thread th = new Thread(tj);
th.start();
for (int i = 0; i < 1000; i++) {
if(i==200){
try {
th.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("我没有vip"+i);
}
}
}
yield()
线程礼让
- 线程礼让就是让当前正在执行的线程暂停,但不阻塞
- 将线程从运行状态转为就绪状态
- 让cpu重新调度,礼让不一定成功!看cpu心情
public class TestYield {
public static void main(String[] args) {
thread thread = new thread();
new Thread(thread,"xiaoming").start();
new Thread(thread,"xiaohong").start();
}
}
class thread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开始");
Thread.yield(); // 礼让: 让当前线程暂停进入就绪状态,等 cpu 重新调度
System.out.println(Thread.currentThread().getName()+"结束");
}
}
isLive()
start()
setPriority()
interrupt(): 不建议使用该方法停止线程。
观测线程状态
线程可以处于以下状态之一,这些状态定义在 Thread.State 中,是一个枚举类型
- NEW: 尚未启动的线程处于此状态
- RUNNABLE: 在Java虚拟机中执行的线程处于次状态
- BLOCKED: 被阻塞等待监视器锁定的线程处于次状态
- WAITING: 正在等待另一个线程执行特定动作的线程处于次状态
- TIMED_WAITING: 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
- TERMINATED: 线程已被终止(已死亡),已退出的线程处于此状态
一个线程可以在给定时间点处于一个状态,这些状态只是 Java 虚拟机状态(不是操作系统状态)
// 首先使用lambda表达式创建了一个新生线程,然后观测该线程输出是新生状态,然后调用state方法,主线程在子线程没有执行完之前一直进行状态监测,当子线程执行完之后继续监测
package Thread;
public class TestState {
public static void main(String[] args) {
// 定义一个子线程,此时还未就绪,是属于新生状态
Thread thread = new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("/");
}
});
Thread.State state = thread.getState();
System.out.println(state); //输出: NEW
thread.start();
state=thread.getState();
System.out.println(state); //输出: RUNNABLE
while (state!=Thread.State.TERMINATED){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
state=thread.getState();
System.out.println(state);//输出: TIMED_WAITING ,因为子线程一开始就睡眠了5秒钟,所以子线程的状态就是一直处于 TIMED_WAITING
}
state=thread.getState();
System.out.println(state);//输出: TERMINATED
thread.start(); // 会爆出异常,因为线程只能执行一次,该线程已经在上面执行过了不能再次启动了
}
}
线程的优先级
线程优先级的设定并不是你给他设置越高他越先调度而是你给他设置越高他的权重越大,最后调度顺序还是看cpu心情,所以优先级低只是意味着获取调度的概率低,而不是优先级低就比优先级高的后调用,这个都是看cpu调度的只是改变一个权重而已。也就是说优先级越高,被 CPU 调度概率/频率就越高,无关顺序。优先级是1-10。
获取或设置线程的优先级:
- setPriority()
- getPriority()
// 给多个线程设置不同优先级,多执行几次,发现优先级越高,越可能先执行,但是并不是绝对的,不是优先级高就一定先执行。
package Thread;
public class TestPriority implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
}
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority()); // 获取主线程的优先级,结果为默认的优先级: 5
TestPriority testPriority = new TestPriority();
Thread t1 = new Thread(testPriority,"aa");
t1.setPriority(Thread.NORM_PRIORITY); // 5
Thread t2 = new Thread(testPriority,"bb");
t2.setPriority(Thread.MAX_PRIORITY); // 10
Thread t3 = new Thread(testPriority,"cc");
t3.setPriority(Thread.MIN_PRIORITY);// 1
Thread t4 = new Thread(testPriority,"dd");
t4.setPriority(6);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
守护线程
- 线程分为用户线程(比如 main 线程)和守护线程
- 虚拟机必须确保用户线程执行完毕,当用户线程执行结束时,守护线程也会结束
- 虚拟机不用等待守护线程执行完毕,比如 gc 线程、后台记录操作日志、监控内存的线程等。
package Thread;
public class TestDaemon {
public static void main(String[] args) {
You you = new You();
God god = new God();
Thread thread = new Thread(god);
thread.setDaemon(true);//默认是false,为用户线程。手动设置为守护线程
thread.start();
new Thread(you).start();
}
}
class You implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("还活着");
}
System.out.println("End World");
}
}
class God implements Runnable{
@Override
public void run() {
// 注意这里是死循环。但如果本线程是守护线程,当用户线程执行结束后,这里的死循环也会被停止
while (true) {
System.out.println("God守护着你");
}
}
}
线程同步(重点)
面试题
处理多线程时,多个线程访问同一个对象(变量或数据),并且某些线程还想修改这个对象,此时就需要线程同步(让线程一个个排队来访问或修改这个对象)。线程同步就是一种等待机制,让需要同时访问此对象的线程进入这个对象的等待池形成队列,等前面线程使用完毕,下个线程再使用。
- 同步: 就是一个一个来,比如排队买东西。
- 并发: 同一个对象被多个线程同时操作,比如10万人同时抢100张火车票。可以使用 sleep() 模拟多个线程并发执行到同一个地方的代码。
- 并发如何解决: 使用线程同步
- 线程同步的形成条件: 队列+锁,就可以解决线程不安全的问题
由于同一个进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题。为了保证数据在方法中被访问的正确性,在访问时加入锁机制,当一个线程获得对象的排它锁,就可以独占资源,其他线程必须等待,当使用完成资源后释放锁即可,但存在以下问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题。比如排队上厕所,如果有个人上厕所10分钟,其他人上只需要1分钟,倒置性能倒置。
三大线程不安全的案例
线程不安全案例1:多人同时买一张票
如果没有线程同步,当只剩下最后一张票的时候,有好几个人同时都看到了这张票,然后他们都买了这种票,最终剩余的票数会小于0,票就会超卖了。
package com.fcj.syn;
//不安全的买票
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
Thread t1 = new Thread(buyTicket,"小黄");
Thread t2 = new Thread(buyTicket,"小红");
Thread t3 = new Thread(buyTicket,"小白");
t1.start();
t2.start();
t3.start();
}
}
class BuyTicket implements Runnable{
// 当前可买的票数
private int ticket = 10;
public boolean flag = true;
@Override
public void run() {
while(flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void buy() throws InterruptedException {
if(ticket<=0){
flag = false;
return ;
}
// 模拟延时
// Thread.sleep(100);
if(ticket>0){
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"买到了第"+(ticket--)+"张票"); // 先执行输出,在执行 ticket = ticket - 1
}
}
}
线程不安全案例2: 多人同时从一个银行账户里取钱
多执行几次,发现可能账户上会出现负数,也就是多取钱了。
package syn;
// 银行账户
class Account{
int money;
String name;
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
class Bank extends Thread{
Account account;
int drawingMoney; // 取多少钱
public Bank(Account account, int drawingMoney, String name) {
super(name); // 调用父类构造器,也就是 Thread 的构造器,传一个名字作为线程的 name
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
if (account.money-drawingMoney<0) {
System.out.println("钱不够了");
return;
}
// 这里延时一下,模拟并发情况下,多个人同时都能走到这里的代码
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money = account.money - drawingMoney; // 取钱后账户内余额 = 上次取钱后的余额 - 本次取多少钱
System.out.println(Thread.currentThread().getName() + "取了" + drawingMoney + ",还剩" + account.money);
}
}
public class UnSafeBank {
public static void main(String[] args) {
// 本来卡里只有100,最后发现并发情况下,两个人可以取出钱,也就是取出了150
Account account = new Account(100,"结婚基金");
Bank you = new Bank(account,100,"你");
Bank youWife = new Bank(account,50,"妻子");
you.start();
youWife.start();
}
}
线程不安全案例3: 并发访问 ArrayList
面试题
多个线程同时访问一个 ArrayList 对象时,可能会有并发安全问题,比如两个线程同时添加元素,后一个元素覆盖了前一个元素,也就是添加了2或n个元素,最后只有一个元素保留下来了。
//不安全的线程
public class UnsafeLIst {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
Thread.sleep(10000); // 模拟等待线程相加结束,再输出查看
System.out.println(list.size()); // 最后发现数据量总是不足10000,原因是有可能在同一时间两个线程加入到了list集合的同一个位置。后面一个线程添加的数据覆盖了上面的一个线程所添加的数据,所以list集合中的数量就要小于10000了。
}
}
同步
同步方法
- 由于我们通过 private 关键字来保证数据对象只能被方法访问,所以我们需要针对方法提出一套机制,这套机制就是 synchronize 关键字,它包括两种用法: synchronized 方法和 synchronized 块。同步方法:
public synchronized void method(int args){…}
- synchronized 方法控制对“对象”的访问,每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁才能执行,否则线程会堵塞,方法一旦执行,就单独占据该锁,直到该方法执行结束才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。缺陷:若将一个大的方法(就是一个里面有很多代码,也可能需要执行很久的方法)声明为 synchronized 将会影响效率,因为这个方法里面可能有些代码只是读取资源,只读取资源(变量)的代码不需要锁,锁了就浪费时间了。
同步块(重点,常用)
- 同步块的写法:
synchronized(obj){…}
,Obj称为同步监视器- obj可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class
- 同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问。
- 第一个线程访问完毕,解锁同步监视器。
- 第二个线程访问,发现同步监视器没有锁,然后锁定访问。
反正记住: 需要锁住的是变化的量和变化的量前面用来判断业务能否继续进行的业务代码
synchronized 关键字的三种用法:
- 修饰静态方法:
- 修饰成员方法:
- 修饰代码块:
安全的线程1: 买票(线程排队执行买票)
package com.fcj.syn;
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
Thread t1 = new Thread(buyTicket,"小黄");
Thread t2 = new Thread(buyTicket,"小红");
Thread t3 = new Thread(buyTicket,"小白");
t1.start();
t2.start();
// t3.setPriority(10);
t3.start();
}
}
class BuyTicket implements Runnable{
private int ticket = 10;
public boolean flag = true;
@Override
public void run() {
while(flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 使用 synchronized 关键字来修饰的方法就叫同步方法,锁的是 this ,也就是当前类的对象,当前类的对象执行完该方法后,别的线程才能执行该方法
public synchronized void buy() throws InterruptedException {
if(ticket<=0){
flag = false;
return ;
}
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"买到了第"+ticket--+"张票");
}
}
安全的线程2: 银行取钱(多人同时从一个账户中取钱)
package com.fcj.syn;
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(100, "银行卡");
// 多个线程共享 account ,并在线程里修改 account 中的余额
Drawing d1 = new Drawing(account, 50, "老大");
Drawing d2 = new Drawing(account, 100, "老二");
d1.start();
d2.start();
}
}
class Account {
int money;//余额
String name;
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
class Drawing extends Thread {
Account account;//账户中的钱
int drawingMoney;
int nowMoney;//手上拿着的钱
public Drawing(Account account, int drawingMoney, String name) {
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
// 取钱
@Override
public void run() {
// public synchronized void run() { // 这么写还是不能解决并发问题,因为如果将 synchronized 用在这里的方法上,锁的就是这个方法的对象,也就是 Drawing 这个类的对象,但实际需要锁的是被修改数据的对象,所以也就是应该锁 account 这个变量和 account 这个变量所在的逻辑判断代码,类似于数据库的事务,才能保证业务正常。如果只是锁了 run() 所在的 this 对象,是不能解决 account 所指向的对象的并发性问题。
synchronized (account) { // 使用同步代码块,锁住要变化的量的对象/共享资源/共享的对象(也就是需要增删改的对象,查的对象不用锁),也就是账户 account ,免得 account 中的余额修改产生并发问题。
if (account.money - drawingMoney < 0) {
System.out.println(Thread.currentThread().getName() + "账户钱不够了");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money = account.money - drawingMoney;
nowMoney = nowMoney + drawingMoney;
System.out.println(account.name + "余额为" + account.money);
System.out.println(this.getName() + "手里的钱" + nowMoney);
}
}
}
安全的线程3: 并发访问 ArrayList
package com.fcj.syn;
import java.util.ArrayList;
import java.util.List;
public class UnsafeLIst {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
synchronized (list){
list.add(Thread.currentThread().getName());
}
}).start();
}
Thread.sleep(3000);
System.out.println(list.size());
}
}
死锁
死锁: 多个线程互相抱着对方需要的资源,且互相等待其他线程占有的资源才能继续运行,而对于自己已有的资源不愿意放手,然后形成僵持。某一个同步块同时拥有两个以上对象的锁时,就可能会发生死锁问题
产生死锁的四个必要条件(我们只要想办法破解其中的任意一个或多个条件就可以避免死锁发生):
- 互斥条件: 一个资源每次只能被一个进程使用
- 请求与保持条件: 一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件: 一个进程获取了一个资源,在该进程用完该资源之前,不能强行把该资源拿走
- 循环等待条件: 若干线程之间形成一种头尾相接的循环等待资源关系。(即多个线程形成了环状的资源要求,就是我需要你这个线程的资源,你需要他那个线程的资源,他需要我这个线程的资源)
// 死锁案例:
package com.fcj.syn;
public class DeadLock {
public static void main(String[] args) throws InterruptedException {
MakeUp g1 = new MakeUp(0,"yf1");
MakeUp g2 = new MakeUp(1,"yf2");
g1.start();
// Thread.sleep(2000);
g2.start();
}
}
//口红
class LipStick{
}
//镜子
class Mirror{
}
class MakeUp extends Thread{
//static保证资源只有一份
private static Mirror mirror = new Mirror();
private static LipStick lipStick = new LipStick();
private int choice;
private String name; // 使用化妆品的人
public MakeUp(int choice,String name){
this.choice = choice;
this.name = name;
}
@Override
public void run() {
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void makeup() throws InterruptedException {
// 一个人先拿镜子,然后拿住一会儿,再拿口红
// 另一个人先拿口红,然后拿住一会儿,再拿镜子
// 这就造成了,第一个人拿了镜子、第二个人拿了口红后。第一个人想拿口红的时候发现口红被第二个人拿了,于是第一个人不动了;而第二个人想拿镜子的时候发现镜子被第一个人拿了,于是第二个人也不动了
if(choice == 0){
synchronized (mirror){
System.out.println(this.name+"拿到了镜子");
sleep(2000);
synchronized (lipStick){
System.out.println(this.name+"拿到了口红");
}
}
}else{
synchronized (lipStick){
System.out.println(this.name+"拿到了口红");
sleep(2000);
synchronized (mirror){
System.out.println(this.name+"拿到了镜子");
}
}
}
}
}
Lock 锁
- 从 JDK5.0 开始, java 提供了更强大的线程同步机制–通过显示定义同步锁对象来实现同步。同步锁使用 lock 对象充当
- Lock 接口是控制多线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象。
- ReentrantLock (可重入锁)类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock ,可以显示的加锁、释放锁。
package com.fcj.syn;
import java.util.concurrent.locks.ReentrantLock;
public class TestLock {
public static void main(String[] args) {
TestLock2 lock2 = new TestLock2();
Thread t1 = new Thread(lock2,"a");
Thread t2 = new Thread(lock2,"b");
Thread t3 = new Thread(lock2,"c");
t1.start();
t2.start();
t3.start();
}
}
class TestLock2 implements Runnable{
private int ticketNums = 1000;
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try{
lock.lock(); // 在 try 代码块的第一行开启锁,try 里面就是有并发安全隐患的代码
if(ticketNums>0){
System.out.println(Thread.currentThread().getName()+"拿到了第"+ticketNums--+"张票");
}else{
break;
}
}finally {
lock.unlock(); //关闭锁,一般都将unlock写入到finally块中
}
}
}
}
lock 锁和 synchronized 的区别
- lock是显式锁(需要手动开启和关闭锁,别忘记关闭锁) synchronized 是隐式锁,出了作用域自动释放。
- Lock 只有代码块锁,synchronized有代码块锁和方法锁
- 使用 lock 锁, JVM 将花费较少的时间来调度线程(性能更好,并且具有更好的扩展性(提供更多的子类))
- 优先使用顺序: Lock -> 同步代码块 -> 同步方法
锁之间的优先级
- 优先级最高: Lock
- 优先级次之: 同步代码块
- 优先级最低: 同步方法
乐观锁和悲观锁
线程通信/线程协作(生产者消费模式)
Java 提供了几个方法解决线程之间的通信问题(这些方法均是Object类的方法,都只能在同步方法或同步代码块中使用,否则会抛出异常 IllegalMonitorStateException):
- wait(): 让当前线程释放该对象的锁,并让当前线程阻塞。和 sleep() 不同,sleep() 会抱着锁睡眠,而 wait() 会释放锁。在调用 wait() 之后有两种解除阻塞的途径:
- 指定最大阻塞时间
- 被其他线程调用 notify() / notifyAll() 方法唤醒
- wait(long timeout): 指定等待的毫秒数
- notify(): 唤醒一个处于等待状态的线程
- notifyAll(): 唤醒同一个对象上所有调用 wait() 方法的线程,优先级别高的线程优先调度
notify() 和 notifyAll() 的区别
notify和notifyAll之间的关键区别在于notify()只会唤醒一个线程,而notifyAll方法将唤醒所有线程。当你调用 notify() 时,只有一个等待线程会被唤醒而且它不能保证哪个线程会被唤醒,这取决于线程调度器。虽然如果你调用 notifyAll() 方法,那么等待该锁的所有线程都会被唤醒,但是在执行剩余的代码之前,所有被唤醒的线程都将争夺锁。
wait()使用例子
class Lock {
}
class ThreadA extends Thread{
Lock lock;
public ThreadA(Lock lock) {
this.lock = lock;
}
public void run() {
synchronized (lock) {
System.out.println("ThreadA线程:获得lock锁");
System.out.println("ThreadA线程:执行结束释放lock锁");
}
}
}
class ThreadB extends Thread{
Lock lock;
public ThreadB(Lock lock) {
this.lock = lock;
}
public void run() {
synchronized (lock) {
System.out.println("ThreadB线程:获得lock锁");
System.out.println("ThreadB线程:执行结束释放锁");
}
}
}
public class waitTest {
public static void main(String[] args) {
Lock lock = new Lock();
ThreadA threadA = new ThreadA(lock);
ThreadB threadB = new ThreadB(lock);
synchronized(lock) { // 主线程这里已经获得了 lock 对象的锁
try {
System.out.println("主线程:启动threadA线程");
threadA.start(); // 因为主线程有 lock 的锁,所以这里的子线程就算 start() 在 run() 方法要获得 lock 的锁也获得不了,只能阻塞等待锁释放
System.out.println("主线程:启动threadB线程");
threadB.start(); // 因为主线程有 lock 的锁,所以这里的子线程就算 start() 在 run() 方法要获得 lock 的锁也获得不了,只能阻塞等待锁释放
System.out.println("主线程:此时lock锁在主线程这里");
System.out.println("主线程:调用wait方法释放锁");
lock.wait(1); // 主线程释放锁,并阻塞 1 毫秒。此时执行到这里,其他两个线程可以相继获得 lock 对象的锁,执行他们的 run() 方法了
System.out.println("主线程:wait方法结束,重新获得lock锁");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
// 输出结果:
// 主线程:启动threadA线程
// 主线程:启动threadB线程
// 主线程:此时lock锁在主线程这里
// 主线程:调用wait方法释放锁
// ThreadA线程:获得lock锁
// ThreadA线程:执行结束释放lock锁
// ThreadB线程:获得lock锁
// ThreadB线程:执行结束释放锁
// 主线程:wait方法结束,重新获得lock锁
wait() 和 notify() 使用例子
@Log4j
public class WaitTest {
public static void main(String[] args) {
Object lock = new Object();
Thread threadA = new Thread(() -> {
synchronized (lock) {
log.info("线程A - 获取了锁");
try {
log.info("线程A - 休眠一会儿");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("线程A - 调用wait..");
try {
lock.wait(); // 这里 wait() 后,代码就阻塞在这里了,并且释放了 lock 对象的锁,其他线程就可以拿到锁了
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("线程A - 被唤醒");
}
}, "A");
threadA.start();
Thread threadB = new Thread(()->{
synchronized (lock) { // 上面的线程调用了 lock.wait() 后,释放了 lock 对象的锁,就可以执行进来这里的代码块了
log.info("线程B - 获得了锁");
log.info("线程B - 叫醒A");
lock.notify(); // 执行 lock.notify() 后,其他线程因为 lock.wait() 阻塞的代码就会被唤醒,继续往下执行
}
}, "B");
threadB.start();
}
}
/*
执行结果:
线程A - 获取了锁
线程A - 休眠一会儿
线程A - 调用wait..
线程B - 获得了锁
线程B - 叫醒A
线程A - 被唤醒
*/
应用场景
- 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中的的产品取走消费;
- 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走位置;
- 如果仓库中放有产品,则消费者将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止。
分析: 这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件
- 对于生产者,没有生产产品之前,要通知消费者等待。而生产产品之后,有需要马上通知消费者消费。
- 对于消费者,在消费之后,要通知生产者已经消费结束,需要继续生产新产品以供消费
- 在生产者消费者问题中,仅有 synchronized 是不够的
- synchronized 可阻止并发更新同一个共享资源,实现了同步
- synchronized 不能用来实现不同线程之间的消息传递(通信)
实现: 管程法 或 信号灯法
管程法
并发协作模式"生产者/消费者模式" -> 利用缓冲区(相当于一个队列/消息队列)解决:管程法
- 生产者:负责生产数据的模块(可能是方法,对象,线程,进程)
- 消费者:负责处理数据的模块(可能是方法,对象,线程,进程)
- 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”
生产者将生产好的数据放入缓冲区,消费者从缓冲区中拿出数据
/*
代码流程:
+ Productor 和 Consumer 是两个子线程,他们两个同时操作 SynContainer 对象中的 list,下面简称 list
+ Consumer 线程: 当开启 Consumer 线程后,Consumer 线程中会判断 list 是否有元素,有就取出,然后唤醒 Productor 让它生产元素,然后 list 中的元素就少一个,取100次。如果 list 没有元素,Consumer 线程就先阻塞代码,如果有 Productor 线程唤醒(Productor 如果生产了元素,就会唤醒其他线程),再继续往下执行,取出元素。
+ Productor 线程: 当开启 Productor 线程后,list 添加元素没超过指定个数(下面代码设定的是 10 个),就往里面添加元素,添加 100 次,每添加一次后就通知其他线程(Consumer 线程) 继续执行
*/
package com.fcj.communication;
//测试生产者消费者模型-->利用缓冲区解决:管程法
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
//生产者,消费者,产品,缓冲区
public class TestPC {
public static void main(String[] args) {
SynContainer container = new SynContainer();
Productor productor = new Productor(container);
Consumer consumer = new Consumer(container);
productor.start();
consumer.start();
}
}
// 生产者线程: 在子线程生产100只鸡,并放到容器(缓冲区)中
class Productor extends Thread{
private SynContainer container;
public Productor(SynContainer container) {
this.container = container;
}
// 生产(烤鸡)
@Override
public void run() {
for (int i = 0; i < 100; i++) {
container.push(new Chicken(i));
System.out.println("生产了第"+i+"只鸡");
}
}
}
// 消费者线程: 在子线程从容器(缓冲区)中取出(消费)100只鸡
class Consumer extends Thread{
private SynContainer container;
public Consumer(SynContainer container) {
this.container = container;
}
// 消费
@Override
public void run() {
for (int i = 0; i < 100; i++) {
container.pop();
System.out.println("消费了第"+i+"只鸡");
}
}
}
// 产品
class Chicken{
int id;//产品编号
public Chicken(int id) {
this.id = id;
}
}
// 缓冲区
class SynContainer{
// 缓冲区容器
List<Chicken> list = new ArrayList<>();
// 缓冲区计数器。这里设定 list 里的元素不能超过 10 个
static final int MAXSIZE = 10;
//生产者放入产品
public synchronized void push(Chicken chicken) throws InterruptedException {
if(list.size() == MAXSIZE){ // 如果满了就让生产者线程一直循环等待。当消费者线程拿出了产品之后,再进行生产
this.wait(); // 通知当前线程停止运行,并释放当前对象的锁。也就是如果发现 list 对象满了, push() 方法就先在这里阻塞,不会继续往下执行。因为 push() 调用的同时,消费者的线程同时也在调用 pop(),所以当 pop() 被调用,list 元素减少后,会执行 notifyAll(),此时代码再从这里继续往下执行。完成了一个取出,一个生产的动作。
}
if(list.size()<MAXSIZE){
// 没有满就放入产品
list.add(chicken);
}
// 通知消费者消费
this.notifyAll();
}
//消费者消费产品
public synchronized Chicken pop() throws InterruptedException {
// 判断能否消费
if(list.size()==0){
// 等待生产者生产
this.wait();
}
// 如果可以消费
if(list.size()>0){
Chicken chicken = list.remove(0);
// 吃完了,通知生产者生产
this.notifyAll();
return chicken;
}
return null;
}
}
信号灯法
并发协作模式"生产者/消费者模式" -> 设置一个标志位解决:信号灯(设置一个标志位,根据标志位判断什么时候等待,什么时候通行)
package src.thread;
//测试生产者消费者问题2:信号灯法,标志位解决
/*
有演员(生产者)和观众(消费者)两个子线程,设定一个标志位判断是否应该执行观众观看节目,还是演员表演。
+ 标志位为 true 时,观众线程就执行 wait() 阻塞代码执行,等待演员线程 notify()/notifyAll() 唤醒,再继续往下执行观看代码,
+ 标志位为 false 时,演员线程就执行 wait() 阻塞代码执行,等待观众线程 notify()/notifyAll() 唤醒,再继续往下执行生产表演节目代码,
*/
public class TestPC2 {
public static void main(String[] args) {
TV tv = new TV();
new Player(tv).start();
new Watcher(tv).start();
}
}
//生产者-->演员
class Player extends Thread {
TV tv;
public Player(TV tv) { //构造方法
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
this.tv.play("快乐大本营播放中");
} else {
this.tv.play("抖音:记录美好生活");//广告
}
}
}
}
//消费者-->观众
class Watcher extends Thread {
TV tv;
public Watcher(TV tv) { //构造方法
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
//产品-->节目
class TV {
//演员表演,观众等待 True
//观众观看,演员等待 False
String voice;//表演的节目
boolean flag = true; //设置标志位,判断演员是否表演,true 就是演员应该要表演,false 就是观众观看节目,演员等待
//表演
public synchronized void play(String voice) {
if (!flag) { //若观众观看的时候,演员需等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员表演了:" + voice);
//通知观众观看
this.notifyAll();//通知(唤醒)
this.voice = voice;
this.flag = !this.flag;
}
//观看
public synchronized void watch() {
if (flag) { //若演员表演的时候。观众需等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观众观看了:" + voice);
//观众观看完通知演员表演
this.notifyAll();
this.flag = !this.flag;
}
}
线程池(TreadPool)(重点)
- 背景: 频繁的创建和销毁会比较耗费计算机资源,比如并发情况下的线程,对性能影响很大。比如每次去公园骑单车,不用每次都买一辆,而是去公园里的 租车公司(相当于线程池) 租一辆,用完单车就放回租车公司,就不浪费了。
- 解决: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建和销毁、实现重复利用。
- 好处:
- 提高响应速度(减少了创建新线程的时间)
- 减低资源消耗(重复利用线程池中的线程)
- 便于线程管理,比如下面的一些设置:
- 可以设置线程池的属性
- corePoolSize: 核心池的大小
- maximumPoolSize: 最大线程数
- keepAliveTime: 线程没有任务时最多保持多久时间后会终止
线程池相关的 API : ExecutorService(真正的线程池接口,常见子类: ThreadPoolExecutor) 和 Executors(工具类,用于创建并返回不同类型的线程池)
ExecutorService
- void execute(Runnable command)︰执行任务/命令,没有返回值,一般用来执行Runnable
- Future submit(Callable task):执行任务,有返回值,一般用来执行Callable
- void shutdown():关闭连接池
Executors
Executors.newFixedThreadPool(n);
: 创建一个可重用固定线程数的线程池Executors.newCachedThreadPool();
: 创建一个可根据需要创建新线程的线程池Executors.newSingleThreadExecutor();
: 创建一个只有一个线程的线程池Executors.newScheduledThreadPool(n);
: 创建一个线程池,它可安排在给定延迟后运行命令或定期的执行
package com.fcj.senior;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 测试线程池
*/
public class TestPool {
public static void main(String[] args) {
/**
* 1、创建服务,创建线程池
* newFixedThreadPool(),参数为线程池大小
*/
ExecutorService service = Executors.newFixedThreadPool(10);
//2、执行
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
//3、关闭连接
service.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
网络编程详解
ip
- B/S编程(Browser/Server): 浏览器/服务器 编程
- C/S编程(Client/Server): 客户端/服务器 编程
TCP/IP参考模型: OSI七层网络模型
网络变成中的2个主要问题
- 如何准确定位到网络上的一台或多台主机: 找到IP和端口号
- 找到主机之后如何通信: 网络通信协议
java.net.IntAddress 此类用于表示 Internet 协议 ip 地址。
ip: 用于表示唯一一台网络上的计算机。
ip 地址的分类
- IPV4: 由 4 个字节组成,每个字节是8位,所以每个字节只能表示0-255,
- IPV6: 128位,由8个无符号整数组成。
ABCD类地址
InetAddress inetAddress1 = InetAddress.getByName("127.0.0.1");
InetAddress inetAddress2 = InetAddress.getByName("localhost");
InetAddress inetAddress3 = InetAddress.getByName("www.baidu.com");
// inetAddress3.若干方法();
inetAddress3.getAddress();
inetAddress3.getCanoicalHostName();
inetAddress3.getHostAddress(); // ip
inetAddress3.getHostName(); // 域名,或自己的电脑名字(我的电脑右键显示的我的电脑的名字)
端口
端口表示计算机上的一个程序的进程,像PID就是port ID,所以端口或一台计算机的可执行程序个数都是65535
Socket 编程
TCP
上次握手
四次挥手
面向连接
UDP
无连接
Packet
URL
初识 Tomcat
聊天通信
文件上传
GUI编程(选学)
AWT
- Frame
- 监听事件
- 鼠标
- 键盘
- 窗口
- 文本框
- 动作事件
Swing
文本框
标签
按钮
文本域
面板
布局方式
关闭窗口
列表
GUI 游戏编程项目-贪吃蛇
- Timer
- 键盘监听
- 游戏帧的概念
注解和反射
注解
- 元注解
- 内置注解
- 反射读取注解
反射
- Class 类
- newInstance()
- 类加载机制
- Method 类
- invoke(…)
- 因为方法存在重载,所以获取的时候也需要写参数的类型
- Field 类
- set(…)
- 因为字段存在继承,所以也需要写参数的类型
- Contruct 类
- newInstance()
- 获取的时候需要传递参数的 Class 类型
- 破坏私有的关键字: setAccessible(true)
- 性能分析: 正常使用 new 关键字创建对象的效率最高,其次是检测关闭的反射,效率最低的是默认的反射
- 可以通过反射获得注解、泛型
JUC 并发编程
JUC 包里有很多类型是并发安全的,也就是它里面的类型已经帮我们维护好了并发问题的解决方案,比如下面的 CopyOnWriteArrayList 就不会有像 ArrayList 的并发安全问题。
package com.sean.base.threadStudy;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 测试JUC安全类型的集合
*/
public class JUCDemo {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(list.size());
}
}
什么是 JUC
线程和进程
8锁现象
集合类不安全
常用的辅助类(必会)
读写锁
阻塞队列(生产者消费者模式?)
四大函数式接口 (必需掌握)
Stream 流式计算
ForkJoin
异步回调
JMM
Volatile
彻底玩转单例模式
深入理解CAS
原子引用
各种锁的理解:公平锁、非公平锁、可重入锁、自旋锁、死锁
JVM入门
前端基础
HTML
CSS
JavaScript
MySQL
MySQL基础+高级
JDBC
UML类图
数据库设计
JavaWeb
Tomcat
HTTP协议
Maven
Gradle
Servlet
JSTL、EL表达式
Cookie/Session
JSP
MCV三层架构
Filter过滤器
监听器
文件上传
邮件收发
富文本编辑器
原生Web应用开发
附1: 注解汇总
原生注解
- @Deprecated: 不建议使用该方法/字段。比如 Thread 类中的 destroy() 方法就有该注解
第三方库中注解
附2: 接口或工具类
接口
- CacheStore: 实现缓存
常用代码块
生成指定长度的随机字符串
//生成指定长度的随机字符串: length用户要求产生字符串的长度
public static String getRandomString(int length){
String str="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random random=new Random();
StringBuffer sb=new StringBuffer();
for(int i=0;i<length;i++){
int number=random.nextInt(62);
sb.append(str.charAt(number));
}
return sb.toString();
}
清空字符串中的所有空白字符
String telphone = " asd ld fj ".replaceAll("\\s+","");
Q.E.D.