Fork me on GitHub

java入门

Java 基础

IDEA 快捷键

快捷键功能
Alt+Enter导入包,自动修正代码
Ctrl+Y删除光标所在行
Ctrl+D复制光标所在行的内容,插入光标位置下面
Ctrl+Alt+L格式化代码
Ctrl+/单行注释
Ctrl+Shift+/选中代码注释,多行注释,再按取消注释
Alt+Ins自动生成代码,toString,get,set等方法(牢记)
Alt+Shift+上下箭头移动当前代码行
Ctrl+Alt+v根据右边值生成左边变量(牢记)

数据类型

数据类型分类

Java的数据类型分为两大类:

  • 基本数据类型:包括 整数浮点数字符布尔
  • 引用数据类型:包括 数组接口

基本数据类型

四类八种基本数据类型:

数据类型关键字内存占用取值范围
字节型byte1个字节-128~127
短整型short2个字节-32768~32767
整型int(默认)4个字节-231次方~2的31次方-1
长整型long8个字节-2的63次方~2的63次方-1
单精度浮点数float4个字节1.4013E-45~3.4028E+38
双精度浮点数double(默认)8个字节4.9E-324~1.7977E+308
字符型char2个字节0-65535
布尔类型boolean1个字节true,false

Java中的默认类型:整数类型是 int 、浮点类型是 double。\
long类型:建议数据后加L表示。\
float类型:建议数据后加F表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Variable {
public static void main(String[] args){
//定义字节型变量
byte b = 100;
System.out.println(b);
//定义短整型变量
short s = 1000;
System.out.println(s);
//定义整型变量
int i = 123456;
System.out.println(i);
//定义长整型变量
long l = 12345678900L;
System.out.println(l);
//定义单精度浮点型变量
float f = 5.5F;
System.out.println(f);
//定义双精度浮点型变量
double d = 8.5;
System.out.println(d);
//定义布尔型变量
boolean bool = false;
System.out.println(bool);
//定义字符型变量
char c = 'A';
System.out.println(c);
}
}

类型转换

不同范围的类型进行操作时, 会自动进行类型的提升

1
2
3
4
5
6
7
8
public static void main(String[] args) {
int i = 1;
double d = 2.5;
//int类型和double类型运算,结果是double类型
//int类型会提升为double类型
double e = d+i;
System.out.println(e);
}

范围小的类型向范围大的类型提升, byteshortchar 运算时直接提升为 int 。\
byte、short、char‐‐>int‐‐>long‐‐>float‐‐>double

循环

for, while, do while, 增强for

选择

if, switch case,
switch case不推荐, 如果忘写break, 后果很严重, 而且case后面的类型有限,(python只有if, 可以用dict)

一些常用类

Scanner类

Scanner 类来获取输入信息

1
2
3
4
5
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String s = scanner.nextLine();
System.out.println("s = " + s);
}

Random类

Random生成随机数
public int nextInt(int n): 返回一个伪随机数, 范围在0(包括)和指定值n(不包括)之间的int值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    public static void main(String[] args) {
Random random = new Random();
int i = random.nextInt();
int i1 = random.nextInt(5);
System.out.println("i = " + i);
System.out.println("i1 = " + i1);
}
```

## ArrayList类
数组集合类, 连续的, 区别于`LinkedList`

## String类
字符串类, 类`String`中包括用于检查各个字符串的方法,比如用于`比较`字符串,`搜索`字符串,`提取`子字符串以及`创建`具有翻译为大写或小写的所有字符的字符串的副本。

## Arrays类
`java.util.Arrays`此类包含用来`操作数组`的各种方法,比如`排序`和`搜索`等。其所有方法均为`静态方法`,调用起来非常简单。

## Math类
`java.lang.Math`类包含用于执行基本数学运算的方法,如`初等指数`、`对数`、`平方根`和`三角函数`。类似这样的工具类,其所有方法均为静态方法,并且不会创建对象,调用起来非常简单。

## Object类
`java.lang.Object`类是Java语言中的根类,即所有类的父类。它中描述的所有方法子类都可以使用。在对象实例化的时候,最终找的父类就是`Object`。

如果一个类没有特别指定父类, 那么默认则继承自`Object`类。例如:
```Java
public class MyClass /*extends Object*/ {
// ...
}

toString方法

返回该对象的字符串表示, 默认是对象的类型+@+内存地址值, python中的__str__

1
2
3
4
5
6
7
8
9
10
11
public class Person {  
private String name;
private int age;

@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
}

// 省略构造器与Getter Setter
}

equals方法

==比较的是2个对象的地址,而equals比较的是2个对象的内容
指示其他某个对象是否与此对象“相等”, 默认比较内存地址, python中的==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.Objects;

public class Person {
private String name;
private int age;

@Override
public boolean equals(Object o) {
// 如果对象地址一样,则认为相同
if (this == o)
return true;
// 如果参数为空,或者类型信息不一样,则认为不同
if (o == null || getClass() != o.getClass())
return false;
// 转换为当前类型
Person person = (Person) o;
// 要求基本类型相等,并且将引用类型交给java.util.Objects类的equals静态方法取用结果
return age == person.age && Objects.equals(name, person.name);
}
}

Objects类

在刚才重写equals代码中,使用到了java.util.Objects类,那么这个类是什么呢?

在JDK7添加了一个Objects工具类,它提供了一些方法来操作对象,它由一些静态的实用方法组成,这些方法是null-save(空指针安全的)或null-tolerant(容忍空指针的),用于计算对象的hashcode、返回对象的字符串表示形式、比较两个对象。

在比较两个对象的时候,Objectequals方法容易抛出空指针异常,而Objects类中的equals方法就优化了这个问题。方法如下:

  • public static boolean equals(Object a, Object b): 判断两个对象是否相等。

源码:

1
2
3
public static boolean equals(Object a, Object b) {  
return (a == b) || (a != null && a.equals(b));
}

Date类

java.util.Date类 表示特定的瞬间,精确到毫秒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.Date;

public class DateDemo {
public static void main(String[] args) {
System.out.println(new Date());
System.out.println(new Date(0L));
System.out.println(new Date().getTime());
}
}


Sun Sep 29 11:31:36 CST 2019
Thu Jan 01 08:00:00 CST 1970
1569727896597

DateFormat类

java.text.DateFormat 是日期/时间格式化子类的抽象类,我们通过这个类可以帮我们完成日期和文本之间的转换,也就是可以在Date对象String对象之间进行来回转换。

  • 格式化:按照指定的格式,从Date对象转换为String对象
  • 解析:按照指定的格式,从String对象转换为Date对象

构造方法

由于DateFormat为抽象类,不能直接使用,所以需要常用的子类java.text.SimpleDateFormat。这个类需要一个模式(格式)来指定格式化或解析的标准。构造方法为:

  • public SimpleDateFormat(String pattern):用给定的模式和默认语言环境的日期格式符号构造SimpleDateFormat

参数pattern是一个字符串,代表日期时间的自定义格式。

标识字母(区分大小写)含义
y
M
d
H
m
s

常用方法

  • public String format(Date date):将Date对象格式化为字符串。
  • public Date parse(String source):将字符串解析为Date对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DateDemo {
public static void main(String[] args) throws ParseException {
System.out.println(new Date());
System.out.println(new Date(0L));
System.out.println(new Date().getTime());

DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(format.format(new Date()));
System.out.println(format.parse("2019-09-29 11:46:13"));
}
}

Sun Sep 29 11:47:14 CST 2019
Thu Jan 01 08:00:00 CST 1970
1569728834328
2019-09-29 11:47:14
Sun Sep 29 11:46:13 CST 2019

System类

java.lang.System 类中提供了大量的静态方法,可以获取与系统相关的信息或系统级操作,在System类的API文档中,常用的方法有

  • public static long currentTimeMillis():返回以毫秒为单位的当前时间。
  • public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length):将数组中指定的数据拷贝到另一个数组中。

currentTimeMillis方法

currentTimeMillis方法就是 获取当前系统时间与1970年01月01日00:00点之间的毫秒差值

1
System.out.println(System.currentTimeMillis());

arraycopy方法

public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length):将数组中指定的数据拷贝到另一个数组中。

数组的拷贝动作是系统级的,性能很高。System.arraycopy方法具有5个参数,含义分别为:

参数序号参数名称参数类型参数含义
1srcObject源数组
2srcPosint源数组索引起始位置
3destObject目标数组
4destPosint目标数组索引起始位置
5lengthint复制元素个数
1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.Arrays;

public class Demo11SystemArrayCopy {
public static void main(String[] args) {
int[] src = new int[]{1,2,3,4,5};
int[] dest = new int[]{6,7,8,9,10};
System.arraycopy( src, 0, dest, 0, 3);
/*代码运行后:两个数组中的元素发生了变化
src数组元素[1,2,3,4,5]
dest数组元素[1,2,3,9,10]
*/
}
}

StringBuilder类

由于String类的对象内容不可改变,所以每当进行字符串拼接时,总是会在内存中创建一个新的对象, 当进行多次拼接时, 会造成时间和空间上的浪费

在API中对String类有这样的描述:字符串是常量,它们的值在创建后不能被更改

StringBuilder被称为可变字符序列,它是一个类似于String的字符串缓冲区,是一个容器, 可以装很多字符串, 并且能够对其中的字符串进行各种操作。默认开辟16字符空间,超过自动扩充

构造方法

常用的两个

  • public StringBuilder():构造一个空的StringBuilder容器。
  • public StringBuilder(String str):构造一个StringBuilder容器,并将字符串添加进去。
1
2
3
4
5
6
7
8
9
10
public StringBuilder() {
super(16);
}

...

public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}

常用方法

public StringBuilder append(...):添加任意类型数据的字符串形式,并返回当前对象自身。
public String toString():将当前StringBuilder对象转换为String对象。

包装类

Java提供了两个类型系统,基本类型引用类型,使用基本类型在于效率,然而很多情况,会创建对象使用,因为对象可以做更多的功能,如果想要我们的基本类型像对象一样操作,就可以使用基本类型对应的包装类,如下:

基本类型对应的包装类(位于java.lang包中)
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean

File类

java.io.File 类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。

构造方法

  • public File(String pathname) :通过将给定的路径名字符串转换为抽象路径名来创建新的 File实例。
  • public File(String parent, String child) :从父路径名字符串和子路径名字符串创建新的 File实例。
  • public File(File parent, String child) :从父抽象路径名和子路径名字符串创建新的 File实例。

常用方法

  • public String getAbsolutePath() :返回此File的绝对路径名字符串
  • public String getPath() :将此File转换为路径名字符串。
  • public String getName() :返回由此File表示的文件或目录的名称。
  • public long length() :返回由此File表示的文件的长度。
  • public boolean exists() :此File表示的文件或目录是否实际存在。
  • public boolean isDirectory() :此File表示的是否为目录。
  • public boolean isFile() :此File表示的是否为文件。
  • public boolean createNewFile() :当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。
  • public boolean delete() :删除由此File表示的文件或目录, 如果此File表示目录,则目录必须为空才能删除。
  • public boolean mkdir() :创建由此File表示的目录。
  • public boolean mkdirs() :创建由此File表示的目录,包括任何必需但不存在的父目录。
  • public String[] list() :返回一个String数组,表示该File目录中的所有子文件或目录。
  • public File[] listFiles() :返回一个File数组,表示该File目录中的所有的子文件或目录。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class FileDemo2 {
public static void main(String[] args) {
File dir = new File("e:\\java\\BaseStudy");

System.out.println("list");
//获取当前目录下的文件以及文件夹的名称。
String[] names = dir.list();
assert names != null;
for (String name : names) {
System.out.println(name);
}

System.out.println("listFiles");
//获取当前目录下的文件以及文件夹对象,只要拿到了文件对象,那么就可以获取更多信息
File[] files = dir.listFiles();
assert files != null;
for (File file : files) {
System.out.println(file);
}
}
}

list
.idea
BaseStudy.iml
out
src
listFiles
e:\java\BaseStudy\.idea
e:\java\BaseStudy\BaseStudy.iml
e:\java\BaseStudy\out
e:\java\BaseStudy\src

面向对象

访问修饰符(私有, 保护, 公开, 默认)

publicprotecteddefault(空的)private
同一类中
同一包中(子类与无关类)
不同包的子类
不同包中的无关类

public具有最大权限。private则是最小权限。

编写代码时,如果没有特殊的考虑,建议这样使用权限:

  • 成员变量使用 private ,隐藏细节。
  • 构造方法使用 public ,方便创建对象。
  • 成员方法使用 public ,方便调用方法。

不加权限修饰符,其访问能力与default修饰符相同

static关键字

类变量

static修饰成员变量时,该变量称为类变量。该类的每个对象都共享同一个类变量的值。任何对象都可以更改该类变量的值,但也可以在不创建该类的对象的情况下对类变量进行操作。

类变量:使用 static关键字修饰的成员变量。\
static 数据类型 变量名;

静态方法

static修饰成员方法时,该方法称为类方法 。静态方法在声明中有static,建议使用类名来调用,而不需要创建类的对象。调用方式非常简单。

类方法:使用 static关键字修饰的成员方法,习惯称为静态方法。

1
2
3
修饰符 static 返回值类型 方法名 (参数列表){
// 执行语句
}

静态方法调用的注意事项:
静态方法可以直接访问类变量和静态方法。
静态方法不能直接访问普通成员变量或成员方法。反之,成员方法可以直接访问类变量或静态方法。
静态方法中,不能使用this关键字。
静态方法只能访问静态成员。

调用格式

static修饰的成员可以并且建议通过类名直接访问。虽然也可以通过对象名访问静态成员,原因即多个对象均属于一个类,共享使用同一个静态成员,但是不建议,会出现警告信息。

1
2
3
4
// 访问类变量
类名.类变量名;
// 调用静态方法
类名.静态方法名(参数);

静态原理图解

static 修饰的内容:

  • 是随着类的加载而加载的,且只加载一次。
  • 存储于一块固定的内存区域(静态区),所以,可以直接被类名调用。
  • 它优先于对象存在,所以,可以被所有对象共享。

静态原理图解

静态代码块

静态代码块:定义在成员位置,使用static修饰的代码块{ }。

  • 位置:类中方法外。
  • 执行:随着类的加载而执行且执行一次,优先于main方法和构造方法的执行。
1
2
3
4
5
6
>    public class ClassName{
> static {
> // 执行语句
> }
> }
>

作用:给类变量进行初始化赋值。用法演示,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Game {
public static int number;
public static ArrayList<String> list;

static {
// 给类变量赋值
number = 2;
list = new ArrayList<String>();
// 添加元素到集合中
list.add("张三");
list.add("李四");
}
}

小贴士:
static 关键字,可以修饰变量、方法和代码块。在使用的过程中,其主要目的还是想在不创建对象的情况下,去调用方法。

super和this

父类空间优先于子类对象产生

在每次创建子类对象时,先初始化父类空间,再创建其子类对象本身。目的在于子类对象中包含了其对应的父类空间,便可以包含其父类的成员,如果父类成员非private修饰,则子类可以随意使用父类成员。代码体现在子类的构造方法调用时,一定先调用父类的构造方法。

super和this的含义

  • super :代表父类的存储空间标识(可以理解为父亲的引用)。
  • this :代表当前对象的引用(谁调用就代表谁)。

子类的每个构造方法中均有默认的super(),调用父类的空参构造。手动调用父类构造会覆盖默认的super()。\
super() 和 this() 都必须是在构造方法的第一行,所以不能同时出现。

继承

封装, 继承, 多态是面向对象的三大特征, Java仅支持单继承, 但可实现多接口

使用继承后, 子类中如果想使用父类中的同名变量时, 需要使用super关键字; 子类使用自身的变量则不需要super关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Fu{
int age=35;
String name="fu";

}

class Zi extends Fu{
int age=10;
String name = "zi";

void show(){
System.out.println("Zi.age = " + age);
System.out.println("Zi.name = " + name);

System.out.println("Fu.age = " + super.age);
System.out.println("Fu.name = " + super.name);
}
}

public class Extend {

public static void main(String[] args) {
Zi zi = new Zi();
zi.show();
}
}


Zi.age = 10
Zi.name = zi
Fu.age = 35
Fu.name = fu

仅可访问非私有父类成员变量, 若想访问父类的私有成员变量, 则需要父类提供getset方法(封装原则)

子类继承父类后, 若要重写父类的方法, 则需要保证权限大于等于父类权限 \
返回值类型、函数名和参数列表都要一模一样。

多态

同一行为, 具有多个不同表现形式.

  1. 继承或者实现【二选一】
  2. 方法的重写【意义体现:不重写,无意义】
  3. 父类引用指向子类对象【格式体现】
1
2
3
4
5
6
父类类型 变量名 = new 子类对象;
变量名.方法名();

// 父类类型:指子类对象继承的父类类型,或者实现的父接口类型。
Fu f = new Zi();
f.method();

当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,执行的是子类重写后方法。

定义父类:

1
2
3
public abstract class Animal {  
    public abstract void eat();  
}

定义子类:

1
2
3
4
5
6
7
8
9
10
11
class Cat extends Animal {  
    public void eat() {  
        System.out.println("吃鱼");  
    }  
}  
 
class Dog extends Animal {  
    public void eat() {  
        System.out.println("吃骨头");  
    }  
}

定义测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
    public static void main(String[] args) {
        // 多态形式,创建对象
        Animal a1 = new Cat();  
        // 调用的是 Cat 的 eat
        a1.eat();          
 
        // 多态形式,创建对象
        Animal a2 = new Dog(); 
        // 调用的是 Dog 的 eat
        a2.eat();               
    }  
}

多态的好处

实际开发的过程中,父类类型作为方法形式参数,传递子类对象给方法,进行方法的调用,更能体现出多态的扩展性与便利。

定义父类:

1
2
3
public abstract class Animal {  
    public abstract void eat();  
}

定义子类:

1
2
3
4
5
6
7
8
9
10
11
class Cat extends Animal {  
    public void eat() {  
        System.out.println("吃鱼");  
    }  
}  
 
class Dog extends Animal {  
    public void eat() {  
        System.out.println("吃骨头");  
    }  
}

定义测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Test {
    public static void main(String[] args) {
        // 多态形式,创建对象
        Cat c = new Cat();  
        Dog d = new Dog(); 
 
        // 调用showCatEat 
        showCatEat(c);
        // 调用showDogEat 
        showDogEat(d); 
 
        /*
        以上两个方法, 均可以被showAnimalEat(Animal a)方法所替代
        而执行效果一致
        */
        showAnimalEat(c);
        showAnimalEat(d); 
    }
 
    public static void showCatEat (Cat c){
        c.eat(); 
    }
 
    public static void showDogEat (Dog d){
        d.eat();
    }
 
    public static void showAnimalEat (Animal a){
        a.eat();
    }
}

引用类型转换

多态的转型分为向上转型与向下转型两种:

向上转型

向上转型:多态本身是子类类型向父类类型向上转换的过程,这个过程是默认的。

当父类引用指向一个子类对象时,便是向上转型。

1
2
父类类型  变量名 = new 子类类型();
如:Animal a = new Cat();

向下转型

向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的。

一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型。

1
2
子类类型 变量名 = (子类类型) 父类变量名;
如:Cat c =(Cat) a;

为什么要转型

当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说,不能调用子类拥有,而父类没有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点”小麻烦”。所以,想要调用子类特有的方法,必须做向下转型。

定义类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
abstract class Animal {  
    abstract void eat();  
}  
 
class Cat extends Animal {  
    public void eat() {  
        System.out.println("吃鱼");  
    }  
    public void catchMouse() {  
        System.out.println("抓老鼠");  
    }  
}  
 
class Dog extends Animal {  
    public void eat() {  
        System.out.println("吃骨头");  
    }  
    public void watchHouse() {  
        System.out.println("看家");  
    }  
}

定义测试类:

1
2
3
4
5
6
7
8
9
10
11
public class Test {
    public static void main(String[] args) {
        // 向上转型  
        Animal a = new Cat();  
        a.eat();                // 调用的是 Cat 的 eat
 
        // 向下转型  
        Cat c = (Cat)a;       
        c.catchMouse();         // 调用的是 Cat 的 catchMouse
    }  
}

转型的异常

1
2
3
4
5
6
7
8
9
10
11
public class Test {
    public static void main(String[] args) {
        // 向上转型  
        Animal a = new Cat();  
        a.eat();               // 调用的是 Cat 的 eat
 
        // 向下转型  
        Dog d = (Dog)a;       
        d.watchHouse();        // 调用的是 Dog 的 watchHouse 【运行报错】
    }  
}

这段代码可以通过编译,但是运行时,却报出了 ClassCastException ,类型转换异常!这是因为,明明创建了Cat类型对象,运行时,当然不能转换成Dog对象的。这两个类型并没有任何继承关系,不符合类型转换的定义。

为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验

1
2
3
变量名 instanceof 数据类型 
如果变量属于该数据类型,返回true
如果变量不属于该数据类型,返回false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
    public static void main(String[] args) {
        // 向上转型  
        Animal a = new Cat();  
        a.eat();               // 调用的是 Cat 的 eat
 
        // 向下转型  
        if (a instanceof Cat){
            Cat c = (Cat)a;       
            c.catchMouse();        // 调用的是 Cat 的 catchMouse
        } else if (a instanceof Dog){
            Dog d = (Dog)a;       
            d.watchHouse();       // 调用的是 Dog 的 watchHouse
        }
    }  
}

接口

接口,是Java语言中一种引用类型,是方法的集合,如果说类的内部封装了成员变量、构造方法和成员方法,那么接口的内部主要就是封装了方法,包含抽象方法(JDK7及以前),默认方法和静态方法(JDK 8),私有方法(JDK 9)。

接口的定义,它与定义类方式相似,但是使用interface关键字。它也会被编译成.class文件,但一定要明确它并不是类,而是另外一种引用数据类型。

引用数据类型:数组接口

接口的使用,它不能创建对象,但是可以被实现(implements类似于被继承)。一个实现接口的类(可以看做是接口的子类),需要实现接口中所有的抽象方法,创建该类对象,就可以调用方法了,否则它必须是一个抽象类。

格式

1
2
3
4
5
6
public interface 接口名称 {
    // 抽象方法
    // 默认方法
    // 静态方法
    // 私有方法
}

抽象方法:使用 abstract关键字修饰,可以省略,没有方法体。该方法供子类实现使用。

1
2
3
public interface InterFaceName {
    public abstract void method();
}

默认方法:使用 default 修饰,不可省略,供子类调用或者子类重写。\
静态方法:使用 static 修饰,供接口直接调用。

1
2
3
4
5
6
7
8
public interface InterFaceName {
    public default void method() {
        // 执行语句
    }
    public static void method2() {
        // 执行语句    
    }
}

私有方法:使用 private 修饰,供接口中的默认方法或者静态方法调用。

1
2
3
4
5
public interface InterFaceName {
    private void method() {
        // 执行语句
    }
}

基本实现

非抽象子类实现接口:

  1. 必须重写接口中所有抽象方法.
  2. 继承了接口的默认方法, 即可以直接调用, 也可以重写.

实现格式:

1
2
3
4
class 类名 implements 接口名 {
    // 重写接口中抽象方法【必须】
    // 重写接口中默认方法【可选】
}

抽象方法的使用: 必须全部实现
默认方法的使用: 可以继承,可以重写,二选一,但是只能通过实现类的对象来调用。
静态方法的使用: 静态与.class文件相关,只能使用接口名调用,不可以通过实现类的类名或者实现类的对象调用
私有方法的使用 : 私有方法:只有默认方法可以调用。私有静态方法:默认方法和静态方法可以调用。

静态方法的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface LiveAble {
    public static void run(){
        System.out.println("跑起来~~~");
    }
}

public class Animal implements LiveAble {
    // 无法重写静态方法
}

public class InterfaceDemo {
    public static void main(String[] args) {
        // Animal.run(); // 【错误】无法继承方法,也无法调用
        LiveAble.run(); // 
    }
}

跑起来~~~

接口的多实现

一个类可以实现多个接口

1
2
3
4
class 类名 [extends 父类名] implements 接口名1,接口名2,接口名3... {
    // 重写接口中抽象方法【必须】
    // 重写接口中默认方法【不重名时可选】
}

抽象方法

接口中,有多个抽象方法时,实现类必须重写所有抽象方法如果抽象方法有重名的,只需要重写一次。

1
2
3
4
5
6
7
8
9
interface A {
    public abstract void showA();
    public abstract void show();
}
 
interface B {
    public abstract void showB();
    public abstract void show();
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class C implements A,B{
@Override
    public void showA() {
        System.out.println("showA");
    }
 
    @Override
    public void showB() {
        System.out.println("showB");
    }
 
    @Override
    public void show() {
        System.out.println("show");
    }
}

默认方法

接口中,有多个默认方法时,实现类都可继承使用。如果默认方法有重名的,必须重写一次。

1
2
3
4
5
6
7
8
9
interface A {
    public default void methodA(){}
    public default void method(){}
}
 
interface B {
    public default void methodB(){}
    public default void method(){}
}

1
2
3
4
5
6
public class C implements A,B{
    @Override
    public void method() {
        System.out.println("method");
    }
}

静态方法

接口中,存在同名的静态方法并不会冲突,原因是只能通过各自接口名访问静态方法。

优先级的问题

当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的默认方法重名,子类就近选择执行父类的成员方法。

定义接口:

1
2
3
4
5
interface A {
    public default void methodA(){
        System.out.println("AAAAAAAAAAAA");
    }
}

定义父类:

1
2
3
4
5
class D {
    public void methodA(){
        System.out.println("DDDDDDDDDDDD");
    }
}

定义子类:

1
2
3
class C extends D implements A {
    // 未重写methodA方法
}

定义测试类:

1
2
3
4
5
6
7
8
9
public class Test {
    public static void main(String[] args) {
        C c = new C();
        c.methodA(); 
    }
}


DDDDDDDDDDDD

接口的多继承

一个接口能继承另一个或者多个接口,这和类之间的继承比较相似。接口的继承使用 extends 关键字,子接口继承父接口的方法。如果父接口中的默认方法有重名的,那么子接口需要重写一次。

定义父接口:

1
2
3
4
5
6
7
8
9
10
11
interface A {
    public default void method(){
        System.out.println("AAAAAAAAAAAAAAAAAAA");
    }
}
 
interface B {
    public default void method(){
        System.out.println("BBBBBBBBBBBBBBBBBBB");
    }
}

定义子接口:

1
2
3
4
5
6
interface D extends A,B{
    @Override
    public default void method() {
        System.out.println("DDDDDDDDDDDDDD");
    }
}

子接口重写默认方法时,default关键字可以保留。 \
子类重写默认方法时,default关键字不可以保留。

小结

  • 接口中,无法定义成员变量,但是可以定义常量,其值不可以改变,默认使用public static final修饰。
  • 接口中,没有构造方法,不能创建对象。
  • 接口中,没有静态代码块

抽象类

父类中的方法,被它的子类们重写,子类各自的实现都不尽相同。那么父类的方法声明和方法主体,只有声明还有意义,而方法主体则没有存在的意义了。我们把没有方法主体的方法称为抽象方法。Java语法规定,包含抽象方法的类就是抽象类。

1
2
3
修饰符 abstract 返回值类型 方法名 (参数列表);

public abstract void run();

如果一个类中含有抽象方法, 那么该类必须是抽象类

1
2
3
public abstract class Animal {
public abstract void run()
}

使用

继承抽象类的子类必须重写父类所有的抽象方法。否则,该子类也必须声明为抽象类。最终,必须有子类实现该父类的抽象方法,否则,从最初的父类到最终的子类都不能创建对象,失去意义。

注意事项

  1. 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。

    理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。

  2. 抽象类中,可以有构造方法,是供子类创建对象时,初始化父类成员使用的。

    理解:子类的构造方法中,有默认的super(),需要访问父类构造方法。

  3. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。

    理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。

  4. 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类。

    理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。

final

用于修饰不可改变内容。
final: 不可改变。可以用于修饰类、方法和变量。

  • :被修饰的类,不能被继承。
  • 方法:被修饰的方法,不能被重写。
  • 变量:被修饰的变量,不能被重新赋值。

修饰类

1
2
3
final class 类名 {
  
}

public final class Stringpublic final class Mathpublic final class Scanner等,都是被final修饰的,目的就是供我们使用,而不让我们随意改变其内容。

修饰方法

1
2
3
修饰符 final 返回值类型 方法名(参数列表){
    //方法体
}

重写被 final 修饰的方法,编译时就会报错。

修饰变量

局部变量 - 基本类型

基本类型的局部变量,被final修饰后,只能赋值一次,不能再更改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class FinalDemo1 {
    public static void main(String[] args) {
        // 声明变量,使用final修饰
        final int a;
        // 第一次赋值 
        a = 10;
        // 第二次赋值
        a = 20// 报错,不可重新赋值
 
 
        // 声明变量,直接赋值,使用final修饰
        final int b = 10;
        // 第二次赋值
        b = 20// 报错,不可重新赋值
    }
}

局部变量 - 引用类型

引用类型的局部变量,被final修饰后,只能指向一个对象,地址不能再更改。但是不影响对象内部的成员变量值的修改

1
2
3
4
5
6
7
8
9
10
11
public class FinalDemo2 {
    public static void main(String[] args) {
        // 创建 User 对象
        final   User u = new User();
        // 创建 另一个 User对象
        u = new User(); // 报错,指向了新的对象,地址值改变。
 
        // 调用setName方法
        u.setName("张三"); // 可以修改
    }
}

成员变量

成员变量涉及到初始化的问题,初始化方式有两种,只能二选一:

  • 显示初始化

    1
    2
    3
    4
    public class User {
        final String USERNAME = "张三";
        private int age;
    }
  • 构造方法初始化

    1
    2
    3
    4
    5
    6
    7
    8
    public class User {
        final String USERNAME ;
        private int age;
        public User(String username, int age) {
            this.USERNAME = username;
            this.age = age;
        }
    }

被final修饰的常量名称,一般都有书写规范,所有字母都大写。

内部类

1
2
3
4
5
class 外部类 {
    class 内部类{
 
    }
}

访问特点

  • 内部类可以直接访问外部类的成员,包括私有成员。
  • 外部类要访问内部类的成员,必须要建立内部类的对象。
1
外部类名.内部类名 对象名 = new 外部类型().new 内部类型();

定义类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Person {
    private  boolean live = true;
    class Heart {
        public void jump() {
            // 直接访问外部类成员
            if (live) {
                System.out.println("心脏在跳动");
            } else {
                System.out.println("心脏不跳了");
            }
        }
    }
 
    public boolean isLive() {
        return live;
    }
 
    public void setLive(boolean live) {
        this.live = live;
    }
 
}

定义测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class InnerDemo {
    public static void main(String[] args) {
        // 创建外部类对象 
        Person p  = new Person();
        // 创建内部类对象
        Person.Heart heart = p.new Heart();
 
        // 调用内部类方法
        heart.jump();
        // 调用外部类方法
        p.setLive(false);
        // 调用内部类方法
        heart.jump();
    }
}


心脏在跳动
心脏不跳了

内部类仍然是一个独立的类,在编译之后会内部类会被编译成独立的.class文件,但是前面冠以外部类的类名和$符号 。
比如,Person$Heart.class

匿名内部类

匿名内部类 :是内部类的简化写法。它的本质是一个带具体实现的 父类或者父接口的 匿名的 子类对象

匿名内部类必须继承一个父类或者实现一个父接口

1
2
3
4
5
6
7
new 父类名或者接口名(){
    // 方法重写
    @Override 
    public void method() {
        // 执行语句
    }
};

定义接口:

1
2
3
public abstract class FlyAble{
    public abstract void fly();
}

创建匿名内部类,并调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class InnerDemo {
    public static void main(String[] args) {
        /*
        1.等号右边:是匿名内部类,定义并创建该接口的子类对象
        2.等号左边:是多态赋值,接口类型引用指向子类对象
        */
        FlyAble  f = new FlyAble(){
            public void fly() {
                System.out.println("我飞了~~~");
            }
        };
 
        //调用 fly方法,执行重写后的方法
        f.fly();
    }
}

通常在方法的形式参数是接口或者抽象类时,也可以将匿名内部类作为参数传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class InnerDemo2 {
    public static void main(String[] args) {
        /*
        1.等号右边:定义并创建该接口的子类对象
        2.等号左边:是多态,接口类型引用指向子类对象
       */
        FlyAble  f = new FlyAble(){
            public void fly() {
                System.out.println("我飞了~~~");
            }
        };
        // 将f传递给showFly方法中
        showFly(f);
    }
    public static void showFly(FlyAble f) {
        f.fly();
    }
}

以上两步,也可以简化为一步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class InnerDemo3 {
    public static void main(String[] args) {          
        /*
        创建匿名内部类,直接传递给showFly(FlyAble f)
        */
        showFly( new FlyAble(){
            public void fly() {
                System.out.println("我飞了~~~");
            }
        });
    }
 
    public static void showFly(FlyAble f) {
        f.fly();
    }
}

引用类型用法

基本类型可以作为成员变量、作为方法的参数、作为方法的返回值,那么当然引用类型也是可以的。

class作为成员变量

interface作为成员变量

interface作为方法参数和返回值类型

接口作为参数时,传递它的子类对象。
接口作为返回值类型时,返回它的子类对象。

native

Java调用非Java代码的接口, 方法由非Java语言实现

集合

集合是java中提供的一种容器,可以用来存储多个数据. 集合的长度是可变的, 可以存储不同的数据类型

单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是java.util.Listjava.util.Set。其中,List的特点是元素有序元素可重复Set的特点是元素无序,而且不可重复List接口的主要实现类有java.util.ArrayListjava.util.LinkedListSet接口的主要实现类有java.util.HashSetjava.util.TreeSet

Collection 常用功能

Collection是所有单列集合的父接口,因此在Collection中定义了单列集合(ListSet)通用的一些方法,这些方法可用于操作所有的单列集合。方法如下:

  • public boolean add(E e):把给定的对象添加到当前集合中 。
  • public void clear(): 清空集合中所有的元素。
  • public boolean remove(E e): 把给定的对象在当前集合中删除。
  • public boolean contains(E e): 判断当前集合中是否包含给定的对象。
  • public boolean isEmpty(): 判断当前集合是否为空。
  • public int size(): 返回集合中元素的个数。
  • public Object[] toArray(): 把集合中的元素,存储到数组中。

Iterator迭代器

JDK提供了一个接口java.util.IteratorIterator接口也是Java集合中的一员,但它与CollectionMap接口有所不同,Collection接口与Map接口主要用于存储元素,而Iterator主要用于迭代访问(即遍历Collection中的元素,因此Iterator对象也被称为迭代器

想要遍历Collection集合,那么就要获取该集合迭代器完成迭代操作.

  • public Iterator iterator(): 获取集合对应的迭代器,用来遍历集合中的元素的。
  • 迭代:即Collection集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续在判断,如果还有就再取出出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。

Iterator接口的常用方法:

  • public E next():返回迭代的下一个元素。
  • public boolean hasNext():如果仍有元素可以迭代,则返回true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class IteratorDemo {
public static void main(String[] args) {
// 使用多态方式 创建对象
Collection<String> coll = new ArrayList<String>();

// 添加元素到集合
coll.add("123");
coll.add("456");
coll.add("789");
//遍历
//使用迭代器 遍历 每个集合对象都有自己的迭代器
Iterator<String> it = coll.iterator();
// 泛型指的是 迭代出 元素的数据类型
while(it.hasNext()){ //判断是否有迭代元素
String s = it.next();//获取迭代出的元素
System.out.println(s);
}
}
}

123
456
789

在进行集合元素取出时,如果集合中已经没有元素了,还继续使用迭代器的next方法,将会发生java.util.NoSuchElementException没有集合元素的错误。

java/util/ArrayList.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/**
* An optimized version of AbstractList.Itr
*/
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;

Itr() {}

public boolean hasNext() {
return cursor != size;
}

@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();

try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

增强for

增强for是一个高级for循环,专门用来遍历数组和集合的。它的内部原理其实是个Iterator迭代器,所以在遍历的过程中,不能对集合中的元素进行增删操作。

1
2
3
for(元素的数据类型  变量 : Collection集合or数组){ 
//写操作代码
}

泛型

泛型通配符

当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符<?>表示。但是一旦使用泛型的通配符后,只能使用Object类中的共性方法,集合中元素自身方法无法使用。

1
2
3
4
5
6
7
8
9
10
public interface Collection<E> extends Iterable<E>{

...

boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean containsAll(Collection<?> c);
...

}

通配符高级使用—-受限泛型

设置泛型的时候,实际上是可以任意设置的,只要是类就可以设置。但是在JAVA的泛型中可以指定一个泛型的上限下限

  • 泛型的上限
    格式: 类型名称 <? extends 类 > 对象名称
    意义: 只能接收该类型及其子类

  • 泛型的下限
    格式: 类型名称 <? super 类 > 对象名称
    意义: 只能接收该类型及其父类型

比如:现已知Object类,String 类,Number类,Integer类,其中Number是Integer的父类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) {
Collection<Integer> list1 = new ArrayList<Integer>();
Collection<String> list2 = new ArrayList<String>();
Collection<Number> list3 = new ArrayList<Number>();
Collection<Object> list4 = new ArrayList<Object>();

getElement(list1);
getElement(list2);//报错
getElement(list3);
getElement(list4);//报错

getElement2(list1);//报错
getElement2(list2);//报错
getElement2(list3);
getElement2(list4);

}
// 泛型的上限:此时的泛型?,必须是Number类型或者Number类型的子类
public static void getElement1(Collection<? extends Number> coll){}
// 泛型的下限:此时的泛型?,必须是Number类型或者Number类型的父类
public static void getElement2(Collection<? super Number> coll){}

List集合

List接口中常用方法

List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法:

  • public void add(int index, E element): 将指定的元素,添加到该集合中的指定位置上。
  • public E get(int index) :返回集合中指定位置的元素。
  • public E remove(int index) : 移除列表中指定位置的元素, 返回的是被移除的元素。
  • public E set(int index, E element) :用指定元素替换集合中指定位置的元素,返回值的更新前的元素。

ArrayList

java.util.ArrayList集合数据存储的结构是数组结构。元素增删慢查找快

LinkedList

java.util.LinkedList 集合数据存储的结构是链表结构, 是一个双向链表。方便元素添加删除的集合。

对一个集合元素的添加与删除经常涉及到首尾操作,而LinkedList提供了大量首尾操作的方法。

  • public void addFirst(E e) : 将指定元素插入此列表的开头。
  • public void addLast(E e) : 将指定元素添加到此列表的结尾。
  • public E getFirst() : 返回此列表的第一个元素。
  • public E getLast() : 返回此列表的最后一个元素。
  • public E removeFirst() : 移除并返回此列表的第一个元素。
  • public E removeLast() : 移除并返回此列表的最后一个元素。
  • public E pop() : 从此列表所表示的堆栈处弹出一个元素。
  • public void push(E e) : 将元素推入此列表所表示的堆栈。
  • public boolean isEmpty() :如果列表不包含元素,则返回true。

Set接口

java.util.Set 接口和 java.util.List 接口一样,同样继承自 Collection 接口,它与 Collection 接口中的方法基本一致,并没有对 Collection 接口进行功能上的扩充,只是比 Collection 接口更加严格了。与 List 接口不同的是, Set 接口中元素无序,并且都会以某种规则保证存入的元素不出现重复。

HashSet

java.util.HashSetSet 接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的(即存取顺序不一致)。 java.util.HashSet 底层的实现其实是一个java.util.HashMap 支持

HashSet 是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取查找性能。保证元素唯一性的方式依赖于: hashCodeequals 方法。

HashSet存储自定义类型元素

HashSet中存放自定义类型元素时,需要重写对象中的hashCodeequals方法,建立自己的比较方式,才能保证HashSet集合中的对象唯一

IDEA自动生成的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import java.util.Objects;

public class Student {
private String name;
private int age;

public Student() {
}

public Student(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age &&
name.equals(student.name);
}

@Override
public int hashCode() {
return Objects.hash(name, age);
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.HashSet;

public class HashSetDemo {
public static void main(String[] args) {
HashSet<Student> hashSet = new HashSet<>();

hashSet.add(new Student("张三", 20));
hashSet.add(new Student("李四", 18));
hashSet.add(new Student("王五", 22));
hashSet.add(new Student("赵六", 25));

for (Student s : hashSet) {
System.out.println(s);
}
}
}

Student{name='张三', age=20}
Student{name='王五', age=22}
Student{name='李四', age=18}
Student{name='赵六', age=25}

LinkedHashSet

java.util.LinkedHashSet, 它是链表和哈希表组合的一个有序数据存储结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.HashSet;
import java.util.LinkedHashSet;

public class LinkedHashSetDemo {
public static void main(String[] args) {
LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
HashSet<String> hashSet = new HashSet<>();

linkedHashSet.add("张三");
linkedHashSet.add("李四");
linkedHashSet.add("王五");
linkedHashSet.add("赵六");

hashSet.add("张三");
hashSet.add("李四");
hashSet.add("王五");
hashSet.add("赵六");

System.out.println("hashSet = " + hashSet.toString());
System.out.println("linkedHashSet = " + linkedHashSet.toString());
}
}

hashSet = [李四, 张三, 王五, 赵六]
linkedHashSet = [张三, 李四, 王五, 赵六]

Collections

java.utils.Collections 是集合工具类,用来对集合进行操作。

  • public static <T> boolean addAll(Collection<T> c, T... elements) :往集合中添加一些元素。
  • public static void shuffle(List<?> list) :打乱集合顺序。
  • public static <T> void sort(List<T> list) :将集合中元素按照默认规则排序。
  • public static <T> void sort(List<T> list,Comparator<? super T> ) :将集合中元素按照自定义的规则排序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.ArrayList;
import java.util.Collections;

public class CollectionsDemo {
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();

// arrayList.add("123");
// arrayList.add("789");
// arrayList.add("654");
// arrayList.add("369");

Collections.addAll(arrayList, "123", "789", "654", "369");

System.out.println("arrayList: " + arrayList.toString());

Collections.sort(arrayList);

System.out.println("arrayList: " + arrayList.toString());
}
}

Comparator比较器

public static <T> void sort(List<T> list) : 将集合中元素按照默认规则排序。

public int compare(String o1, String o2) : 比较其两个参数的顺序。

两个对象比较的结果有三种:大于,等于,小于。
如果要按照升序排序, 则o1 小于o2,返回(负数),相等返回0,01大于02返回(正数) 如果要按照降序排序 则o1 小于o2,返回(正数),相等返回0,01大于02返回(负数)

Comparable和Comparator两个接口的区别

  • Comparable强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。需重写compareTo方法
  • Comparator强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sortArrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。需重写compare方法

Comparable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.util.Objects;

public class Student implements Comparable<Student>{
private String name;
private int age;

Student(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age &&
name.equals(student.name);
}

@Override
public int hashCode() {
return Objects.hash(name, age);
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

@Override
public int compareTo(Student o) {
int res = name.compareTo(o.name);
if (0==res) return age - o.age;
else return res;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.ArrayList;
import java.util.Collections;

public class ComparableDemo {
public static void main(String[] args) {
ArrayList<Student> arrayList = new ArrayList<Student>();

arrayList.add(new Student("123", 22));
arrayList.add(new Student("567", 17));
arrayList.add(new Student("567", 15));
arrayList.add(new Student("346", 20));

System.out.println(arrayList.toString());

Collections.sort(arrayList);

System.out.println(arrayList.toString());
}
}

[Student{name='123', age=22}, Student{name='567', age=17}, Student{name='567', age=15}, Student{name='346', age=20}]
[Student{name='123', age=22}, Student{name='346', age=20}, Student{name='567', age=15}, Student{name='567', age=17}]

Comparator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import java.util.Objects;

public class Student /*implements Comparable<Student>Student*/{
private String name;
private int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

Student(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age &&
name.equals(student.name);
}

@Override
public int hashCode() {
return Objects.hash(name, age);
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

// @Override
// public int compareTo(Student o) {
// int res = name.compareTo(o.name);
// if (0==res) return age - o.age;
// else return res;
// }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

public class ComparatorDemo {
public static void main(String[] args) {

ArrayList<Student> arrayList = new ArrayList<Student>();

arrayList.add(new Student("123", 22));
arrayList.add(new Student("567", 17));
arrayList.add(new Student("567", 15));
arrayList.add(new Student("346", 20));

System.out.println(arrayList.toString());

Collections.sort(arrayList, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
int res = o1.getAge() - o2.getAge();
if (res == 0)return o1.getName().compareTo(o2.getName());
else return res;
}
});

System.out.println(arrayList.toString());
}
}

[Student{name='123', age=22}, Student{name='567', age=17}, Student{name='567', age=15}, Student{name='346', age=20}]
[Student{name='567', age=15}, Student{name='567', age=17}, Student{name='346', age=20}, Student{name='123', age=22}]

map

  • HashMap:存储数据采用的哈希表结构,元素的存取顺序不能保证一致。由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
  • LinkedHashMapHashMap下有个子类LinkedHashMap,存储数据采用的哈希表结构+链表结构。通过链表结构可以保证元素的存取顺序一致;通过哈希表结构可以保证键的唯一不重复,需要重写键的hashCode()方法、equals()方法。

Map接口中的集合都有两个泛型变量,在使用时,要为两个泛型变量赋予数据类型。两个泛型变量的数据类型可以相同,也可以不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

public class MapDemo {

private void addElement(Map<String, String> map){
map.put("张三", "aaa");
map.put("李四", "bbb");
map.put("王五", "ccc");
map.put("赵六", "ddd");
}

private void showHashMap(){
HashMap<String, String> stringHashMap = new HashMap<>();
addElement(stringHashMap);

System.out.println(stringHashMap.toString());
}

private void showLinkedHashMap(){
LinkedHashMap<String, String> stringLinkedHashMap = new LinkedHashMap<>();
addElement(stringLinkedHashMap);

System.out.println(stringLinkedHashMap.toString());
}


public static void main(String[] args) {
new MapDemo().showHashMap();
new MapDemo().showLinkedHashMap();
}
}

{李四=bbb, 张三=aaa, 王五=ccc, 赵六=ddd}
{张三=aaa, 李四=bbb, 王五=ccc, 赵六=ddd}

常用方法

  • public V put(K key, V value): 把指定的键与指定的值添加到Map集合中。
  • public V remove(Object key): 把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。
  • public V get(Object key): 根据指定的键, 在Map集合中获取对应的值, 不存在则返回null
  • public Set<K> keySet(): 获取Map集合中所有的键,存储到Set集合中。
  • public Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)。

使用put方法时,若指定的键(key)在集合中没有,则没有这个键对应的值,返回null,并把指定的键值添加到集合中;若指定的键(key)在集合中存在,则返回值为集合中键对应的值(该值为替换前的值),并把指定键所对应的值,替换成指定的新值。

Entry键值对对象

Map中的一个键值对对象就是一个Entry

  • public K getKey() :获取Entry对象中的键。
  • public V getValue() :获取Entry对象中的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//        Set<Map.Entry<String, String>> entries = stringHashMap.entrySet();
//
// for (Map.Entry<String, String> entry : entries){
// System.out.print(entry.getKey());
// System.out.println(entry.getValue());
// }

for (Map.Entry<String, String> entry : stringHashMap.entrySet()) {
System.out.println("entry = " + entry);
}
```
>Map集合不能直接使用`迭代器`或者`foreach`进行遍历。但是转成`Set`之后就可以使用了。

### HashMap存储自定义类型键值

* 给HashMap中存放自定义对象时,如果自定义对象作为key存在,这时要保证对象唯一,必须复写对象的`hashCode`和`equals`方法
* 如果要保证map中**存放**的key和**取出**的顺序一致,可以使用 `java.util.LinkedHashMap` 集合来存放

# 异常
程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。

异常机制其实是帮助我们找到程序中的问题,异常的根类是 `java.lang.Throwable` ,其下有两个子类:
`java.lang.Error`与`java.lang.Exception`, 平常所说的异常指`java.lang.Exception` 。

`Throwable`体系:
* `Error`: 严重错误Error,无法通过处理的错误,只能事先避免,好比绝症。
* `Exception`: 表示异常,异常产生后程序员可以通过代码的方式纠正,使程序继续运行,是必须要处理的。好比感冒、阑尾炎。


`Throwable`中的常用方法:
* `public void printStackTrace()` :打印异常的详细信息。包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用`printStackTrace`
* `public String getMessage()` :获取发生异常的原因。提示给用户的时候,就提示错误原因。
* `public String toString()` :获取异常的类型和异常描述信息(不用)

![](http://img.iceflower.xyz/java/%E5%BC%82%E5%B8%B8%E7%9A%84%E5%88%86%E7%B1%BB.png)

## 异常的处理

### throw
```Java
throw new NullPointerException("要访问的arr数组不存在");
 
throw new ArrayIndexOutOfBoundsException("该索引在数组中不存在,已超出范围");

Objects非空判断

1
2
3
4
5
6
7
java/util/Objects.java:202

public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}

声明异常throws

1
修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{   }
1
2
3
4
5
6
7
8
9
10
11
12
13
public class ThrowsDemo {
    public static void main(String[] args) throws FileNotFoundException {
        read("a.txt");
    }
 
    // 如果定义功能时有问题发生需要报告给调用者。可以通过在方法上使用throws关键字进行声明
    public static void read(String path) throws FileNotFoundException {
        if (!path.equals("a.txt")) {//如果不是 a.txt这个文件 
            // 我假设  如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常  throw
            throw new FileNotFoundException("文件不存在");
        }
    }
}

捕获异常try…catch

1
2
3
4
5
6
try{
     编写可能会出现异常的代码
}catch(异常类型  e){
     处理异常的代码
     //记录日志/打印异常信息/继续抛出异常
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TryCatchDemo {
    public static void main(String[] args) {
        try {// 当产生异常时,必须有处理方式。要么捕获,要么声明。
            read("b.txt");
        } catch (FileNotFoundException e) {// 括号中需要定义什么呢?
            //try中抛出的是什么异常,在括号中就定义什么异常类型
            System.out.println(e);
        }
        System.out.println("over");
    }
    /*
     *
     * 我们 当前的这个方法中 有异常  有编译期异常
     */
    public static void read(String path) throws FileNotFoundException {
        if (!path.equals("a.txt")) {//如果不是 a.txt这个文件 
            // 我假设  如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常  throw
            throw new FileNotFoundException("文件不存在");
        }
    }
}

finally代码块

finally:有一些特定的代码无论异常是否发生,都需要执行。另外,因为异常会引发程序跳转,导致有些语句执行不到。而finally就是解决这个问题的,在finally代码块中存放的代码都是一定会被执行的。

1
try...catch....finally:自身需要处理异常,最终还得关闭资源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TryCatch{
    public static void main(String[] args) {
        try {
            read("a.txt");
        } catch (FileNotFoundException e) {
            //抓取到的是编译期异常  抛出去的是运行期 
            throw new RuntimeException(e);
        } finally {
            System.out.println("不管程序怎样,这里都将会被执行。");
        }
        System.out.println("over");
    }
    /*
     *
     * 我们 当前的这个方法中 有异常  有编译期异常
     */
    public static void read(String path) throws FileNotFoundException {
        if (!path.equals("a.txt")) {//如果不是 a.txt这个文件 
            // 我假设  如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常  throw
            throw new FileNotFoundException("文件不存在");
        }
    }
}

当只有在try或者catch中调用退出JVM的相关方法,此时finally才不会执行,否则finally永远会执行。

自定义异常

1
2
1. 自定义一个编译期异常: 自定义类 并继承于 java.lang.Exception 。
2. 自定义一个运行时期的异常类:自定义类 并继承于 java.lang.RuntimeException 。

线程和进程

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

一个程序运行后至少有一个进程,一个进程中可以包含多个线程

线程调度:

  • 分时调度
    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
  • 抢占式调度
    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

并发与并行

  • 并发:指两个或多个事件在同一个时间段内发生。
  • 并行:指两个或多个事件在同一时刻发生(同时发生)。

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核CPU,便是多核处理器,核 越多,并行处理的程序越多,能大大的提高电脑运行的效率。

注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

多线程

Java中通过继承Thread类来创建并启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程

多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。
通过调用start()方法来通知JVM来开辟新的线程空间, 并在新的线程空间里面执行对象的run方法
当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。

Thread类

构造方法

  • public Thread() :分配一个新的线程对象。
  • public Thread(String name) :分配一个指定名字的新的线程对象。
  • public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
  • public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

常用方法

  • public String getName() :获取当前线程名称。
  • public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
  • public void run() :此线程要执行的任务在此处定义代码。
  • public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
  • public static Thread currentThread() :返回对当前正在执行的线程对象的引用。

创建线程的方式总共有两种,一种是继承Thread类方式,一种是实现Runnable接口方式

Runnable接口

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Threadtarget来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用线程对象的start()方法来启动线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        //创建自定义类对象  线程任务对象
        MyRunnable mr = new MyRunnable();
        //创建线程对象
        Thread t = new Thread(mr, "小强");
        t.start();
        for (int i = 0; i < 20; i++) {
            System.out.println("旺财 " + i);
        }
    }
}

Runnable对象仅仅作为Thread对象的targetRunnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其targetrun()方法。

Thread和Runnable的区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

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

  1. 适合多个相同的程序代码的线程去共享同一个资源。
  2. 可以避免java中的单继承的局限性。
  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
  4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程。

匿名内部类方式实现线程的创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class NoNameInnerClassThread {
    public static void main(String[] args) {        
//      new Runnable(){
//          public void run(){
//              for (int i = 0; i < 20; i++) {
//                  System.out.println("张宇:"+i);
//              }
//          }  
//      }; //‐‐‐这个整体  相当于new MyRunnable()
        Runnable r = new Runnable(){
            public void run(){
                for (int i = 0; i < 20; i++) {
                    System.out.println("张宇:"+i);
                }
            }  
        };

        new Thread(r).start();
 
        for (int i = 0; i < 20; i++) {
            System.out.println("费玉清:"+i);
        }
    }
}

线程安全

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
多线程如果不加约束, 很容易出问题的.

线程安全问题都是由全局变量静态变量引起的。若每个线程中对全局变量、静态变量只有操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

线程同步

要解决多线程并发访问一个资源的安全性问题,Java中提供了同步机制(synchronized)来解决。

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。

  1. 同步代码块。
  2. 同步方法。
  3. 锁机制。

同步代码块

同步代码块synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

格式:

1
2
3
synchronized(同步锁){
     需要同步操作的代码
}

同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.

  1. 锁对象 可以是任意类型。
  2. 多个线程对象 要使用同一把锁。

在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着
(BLOCKED)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Ticket implements Runnable{
    private int ticket = 100;
    
    Object lock = new Object();
    /*
     * 执行卖票操作
     */
    @Override
    public void run() {
        //每个窗口卖票的操作 
        //窗口 永远开启 
        while(true){
            synchronized (lock) {
                if(ticket>0){//有票 可以卖
                    //出票操作
                    //使用sleep模拟一下出票时间 
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        // TODO Auto‐generated catch block
                        e.printStackTrace();
                    }
                    //获取当前线程对象的名字 
                    String name = Thread.currentThread().getName();
                    System.out.println(name+"正在卖:"+ticket‐‐);
                }
            }
        }
    }
}

同步方法

同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

1
2
3
public synchronized void method(){
    可能会产生线程安全问题的代码
}

同步锁是谁?
对于非static方法,同步锁就是this。
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Ticket implements Runnable{
    private int ticket = 100;
    /*
     * 执行卖票操作
     */
    @Override
    public void run() {
        //每个窗口卖票的操作 
        //窗口 永远开启 
        while(true){
            sellTicket();
        }
    }
    
    /*
     * 锁对象 是 谁调用这个方法 就是谁 
     *   隐含 锁对象 就是  this
     *    
     */
    public synchronized void sellTicket(){
        if(ticket>0){//有票 可以卖   
            //出票操作
            //使用sleep模拟一下出票时间 
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                // TODO Auto‐generated catch block
                e.printStackTrace();
            }
            //获取当前线程对象的名字 
            String name = Thread.currentThread().getName();
            System.out.println(name+"正在卖:"+ticket‐‐);
        }
    }
}

锁机制(Lock锁)

java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

Lock锁也称同步锁,加锁与释放锁方法化了,如下:

public void lock(): 加同步锁。
public void unlock(): 释放同步锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Ticket implements Runnable{
    private int ticket = 100;
    
    Lock lock = new ReentrantLock();
    /*
     * 执行卖票操作
     */
    @Override
    public void run() {
        //每个窗口卖票的操作 
        //窗口 永远开启 
        while(true){
            lock.lock();
            if(ticket>0){//有票 可以卖
                //出票操作 
                //使用sleep模拟一下出票时间 
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    // TODO Auto‐generated catch block
                    e.printStackTrace();
                }
                //获取当前线程对象的名字 
                String name = Thread.currentThread().getName();
                System.out.println(name+"正在卖:"+ticket‐‐);
            }
            lock.unlock();
        }
    }
}

线程状态

API中 java.lang.Thread.State 这个枚举中给出了六种线程状态

线程状态导致状态发生条件
NEW(新建)线程刚被创建,但是并未启动。还没调用start方法。
Runnable(可运行)线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。
Blocked(锁阻塞)当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待)一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
Timed Waiting(计时等待)同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
Teminated(被终止)因为run方法正常退出而死亡, 或者因为没有捕获的异常终止了run方法而死亡。

Timed Waiting(计时等待)

Timed Waiting在API中的描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。

当调用了sleep方法之后,当前执行的线程就进入到“休眠状态”,其实就是所谓的Timed Waiting(计时等待)

实现一个计数器,计数到100,在每个数字之间暂停1秒,每隔10个数字输出一个字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 public class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            if ((i) % 10 == 0) {
                System.out.println("‐‐‐‐‐‐‐" + i);
            }
            System.out.print(i);
            try {
                Thread.sleep(1000);
                System.out.print("    线程睡眠1秒!\n");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        new MyThread().start();
    }
}

  1. 进入TIMED_WAITING状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协作关系
  2. 为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠
  3. sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态

    sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始立刻执行。

BLOCKED(锁阻塞)

Blocked状态在API中的介绍为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。

线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。

这是由Runnable状态进入Blocked状态。除此Waiting以及Time Waiting状态也会在某种情况下进入阻塞状态

Waiting(无限等待)

Wating状态在API中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class WaitingTest {
    public static Object obj = new Object();
 
    public static void main(String[] args) {
        // 演示waiting
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    synchronized (obj){
                        try {
                            System.out.println( Thread.currentThread().getName() +"=== 获取到锁对象,调用wait方法,进入waiting状态,释放锁对象");
                            obj.wait();  //无限等待
                            //obj.wait(5000); //计时等待, 5秒 时间到,自动醒来
 
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println( Thread.currentThread().getName() + "=== 从waiting状态醒来,获取到锁对象,继续执行了");
                    }
                }
            }
        },"等待线程").start();
 
        new Thread(new Runnable() {
            @Override
            public void run() {
//                while (true){   //每隔3秒 唤醒一次
 
                    try {
                        System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 等待3秒钟");
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
 
                    synchronized (obj){
                        System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 获取到锁对象,调用notify方法,释放锁对象");
                        obj.notify();
                    }
                }
//            }
        },"唤醒线程").start();
    }
}

一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的Object.notify() 方法 或 Object.notifyAll()方法。

其实waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。

当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法那么A线程就进入了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。

状态转换

翻阅API的时候会发现Timed Waiting(计时等待) 与 Waiting(无限等待) 状态联系还是很紧密的, 比如Waiting(无限等待) 状态中wait方法是空参的,而timed waiting(计时等待) 中wait方法是带参的。这种带参的方法,其实是一种倒计时操作,相当于我们生活中的小闹钟,我们设定好时间,到时通知,可是 如果提前得到(唤醒)通知,那么设定好时间在通知也就显得多此一举了,那么这种设计方案其实是一举两 得。如果没有得到(唤醒)通知,那么线程就处于Timed Waiting状态,直到倒计时完毕自动醒来;如果在倒 计时期间得到(唤醒)通知,那么线程从Timed Waiting状态立刻唤醒。

等待唤醒机制

线程间通信

最常见的生产者消费者模型, 一个线程生产, 一个线程消费

为什么要处理线程间通信
多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

如何保证线程间通信有效利用资源:
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。

等待唤醒机制

什么是等待唤醒机制
这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。

就是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。

wait/notify就是线程间的一种协作机制。

等待唤醒中的方法
等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:

  1. wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是通知notify在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
  2. notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
  3. notifyAll:则释放所通知对象的 wait set 上的全部线程。

哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。

总结如下:

  • 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
  • 否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态

调用wait和notify方法需要注意的细节

  1. wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
  2. wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
  3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。

线程池

线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

合理利用线程池能够带来三个好处:

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

线程池的使用

Java里面线程池的顶级接口是java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。

Executors类中有个创建线程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads) :返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:

  • public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行

    Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。

使用线程池中线程对象的步骤:

  1. 创建线程池对象。
  2. 创建Runnable接口子类对象。(task)
  3. 提交Runnable接口子类对象。(take task)
  4. 关闭线程池(一般不做)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
        // 创建Runnable实例对象
        MyRunnable r = new MyRunnable();
 
        //自己创建线程对象的方式
        // Thread t = new Thread(r);
        // t.start(); ‐‐‐> 调用MyRunnable中的run()
 
        // 从线程池中获取线程对象,然后调用MyRunnable中的run()
        service.submit(r);
        // 再获取个线程对象,调用MyRunnable中的run()
        service.submit(r);
        service.submit(r);
        // 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
        // 将使用完的线程又归还到了线程池中
        // 关闭线程池
        service.shutdown();
    }
}

Lambda表达式

1
() ‐> System.out.println("多线程任务执行!")
1
2
3
4
5
6
public class RunnableImpl implements Runnable {
    @Override
    public void run() {
        System.out.println("多线程任务执行!");
    }
}

Lambda标准格式

Lambda省去面向对象的条条框框,格式由3个部分组成:
一些参数, 一个箭头, 一段代码

Lambda表达式的标准格式为:

1
(参数类型 参数名称) ‐> { 代码语句 }

小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
->是新引入的语法格式,代表指向动作。
大括号内的语法与传统方法体要求基本一致。

排序时使用lambda表达式

1
2
3
 Arrays.sort(array, (Person a, Person b) ‐> {
            return a.getAge() ‐ b.getAge();
        });

Lambda的使用前提

Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:

  1. 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。无论是JDK内置的 Runnable 、 Comparator 接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。
  2. 使用Lambda必须具有上下文推断。也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。

    有且仅有一个抽象方法的接口,称为“函数式接口”。

多进程

IO

IO的分类

根据数据的流向分为:输入流和输出流。

  • 输入流 :把数据从 其他设备 上读取到 内存 中的流。
  • 输出流 :把数据从 内存 中写出到 其他设备 上的流。

格局数据的类型分为:字节流和字符流。

  • 字节流 :以字节为单位,读写数据的流。
  • 字符流 :以字符为单位,读写数据的流。
顶级父类们输入流输出流
字节流字节输入流 InputStream字节输出流 OutputStream
字符流字符输入流 Reader字符输出流 Writer

字节流

字节输出流【OutputStream】

java.io.OutputStream抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了字节输出流的基本共性功能方法。

  • public void close() :关闭此输出流并释放与此流相关联的任何系统资源。
  • public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。
  • public void write(byte[] b) :将 b.length字节从指定的字节数组写入此输出流。
  • public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。
  • public abstract void write(int b) :将指定的字节输出流。

close方法,当完成流的操作时,必须调用此方法,释放系统资源。

FileOutputStream类

java.io.FileOutputStream 类是文件输出流,用于将数据写出到文件。

  • public FileOutputStream(File file) :创建文件输出流以写入由指定的 File对象表示的文件。
  • public FileOutputStream(String name) : 创建文件输出流以指定的名称写入文件。
  • public FileOutputStream(File file, boolean append) : 创建文件输出流以写入由指定的 File对象表示的文件。
  • public FileOutputStream(String name, boolean append) : 创建文件输出流以指定的名称写入文件
1
2
3
4
5
6
7
8
9
10
public class FileOutputStreamConstructor throws IOException {
    public static void main(String[] args) {
        // 使用File对象创建流对象
        File file = new File("a.txt");
        FileOutputStream fos = new FileOutputStream(file);
      
        // 使用文件名称创建流对象
        FileOutputStream fos = new FileOutputStream("b.txt");
    }
}

字节输入流【InputStream】

java.io.InputStream 抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。

  • public void close() :关闭此输入流并释放与此流相关联的任何系统资源。
  • public abstract int read() : 从输入流读取数据的下一个字节。
  • public int read(byte[] b) : 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。

close方法,当完成流的操作时,必须调用此方法,释放系统资源。

FileInputStream类

java.io.FileInputStream 类是文件输入流,从文件中读取字节。

  • FileInputStream(File file) : 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File对象 file命名。
  • FileInputStream(String name) : 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name命名。

当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有该文件,会抛出 FileNotFoundException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;

public class IOdemo1 {

private void writeToFile() throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream("Iodemo1.txt");
byte[] val = "IO测试文件1".getBytes();

fileOutputStream.write(val);
fileOutputStream.close();
}

private void readFromFile() throws IOException {
FileInputStream fileInputStream = new FileInputStream("Iodemo1.txt");
// 读取一个字节
int i = fileInputStream.read();
System.out.println("i = " + (char)i);

// 在读取一个字节
int o = fileInputStream.read();
System.out.println("o = " + o);
System.out.println("(char)o = " + (char)o);

// 读取12个字节
byte [] tmp = new byte [12];
int read = fileInputStream.read(tmp);
System.out.println("read = " + read);
System.out.println(new String(tmp));

fileInputStream.close();
}

public static void main(String[] args) throws IOException {
IOdemo1 iOdemo1 = new IOdemo1();
iOdemo1.writeToFile();
iOdemo1.readFromFile();

}
}

i = I
o = 79
(char)o = O
read = 12
测试文件

文件复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Copy {
    public static void main(String[] args) throws IOException {
        // 1.创建流对象
        // 1.1 指定数据源
        FileInputStream fis = new FileInputStream("D:\\test.jpg");
        // 1.2 指定目的地
        FileOutputStream fos = new FileOutputStream("test_copy.jpg");
 
        // 2.读写数据
        // 2.1 定义数组
        byte[] b = new byte[1024];
        // 2.2 定义长度
        int len;
        // 2.3 循环读取
        while ((len = fis.read(b))!=‐1) {
            // 2.4 写出数据
            fos.write(b, 0 , len);
        }
 
        // 3.关闭资源
        fos.close();
        fis.close();
    }
}

字符流

当使用字节流读取文本文件时,可能会有一个小问题。就是遇到中文字符时,可能不会显示完整的字符,那是因为一个中文字符可能占用多个字节存储。所以Java提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件。

字符输入流【Reader】

java.io.Reader 抽象类是表示用于读取字符流的所有类的超类,可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法。

  • public void close():关闭此流并释放与此流相关联的任何系统资源。
  • public int read(): 从输入流读取一个字符。
  • public int read(char[] cbuf): 从输入流中读取一些字符,并将它们存储到字符数组 cbuf中 。

FileReader类

java.io.FileReader类是读取字符文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。

  • FileReader(File file):创建一个新的 FileReader,给定要读取的File对象。
  • FileReader(String fileName):创建一个新的 FileReader,给定要读取的文件的名称.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class IOdemo2 {
private void writeToFile() throws IOException {
FileWriter fileWriter = new FileWriter("Iodemo2.txt");
fileWriter.write("Io测试文件2");

fileWriter.close();
}

private void readFromFile() throws IOException {
FileReader fileReader = new FileReader("Iodemo2.txt");

int value;
while((value=fileReader.read())!=-1){
System.out.println("(char)value = " + (char)value);
}

fileReader.close();
}

public static void main(String[] args) throws IOException {
IOdemo2 iOdemo2 = new IOdemo2();
iOdemo2.writeToFile();
iOdemo2.readFromFile();

}
}

(char)value = I
(char)value = o
(char)value = 测
(char)value = 试
(char)value = 文
(char)value = 件
(char)value = 2

字符输出流【Writer】

  • java.io.Writer : 抽象类是表示用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。它定义了字节输出流的基本共性功能方法。
  • void write(int c) 写入单个字符。
  • void write(char[] cbuf) 写入字符数组。
  • abstract void write(char[] cbuf, int off, int len) 写入字符数组的某一部分,off数组的开始索引,len写的字符个数。
  • void write(String str) 写入字符串。
  • void write(String str, int off, int len) 写入字符串的某一部分, off字符串的开始索引, len写的字符个数。
  • void flush() 刷新该流的缓冲。
  • void close() 关闭此流,但要先刷新它。

FileWriter类

java.io.FileWriter 类是写出字符到文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区

  • FileWriter(File file) : 创建一个新的 FileWriter,给定要读取的File对象。
  • FileWriter(String fileName) : 创建一个新的 FileWriter,给定要读取的文件的名称。

缓冲流

缓冲流,也叫高效流,是对4个基本的FileXxx流的增强,所以也是4个流,按照数据类型分类:

  • 字节缓冲流: BufferedInputStreamBufferedOutputStream
  • 字符缓冲流: BufferedReaderBufferedWriter

缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。

字节缓冲流

  • public BufferedInputStream(InputStream in) :创建一个新的缓冲输入流。
  • public BufferedOutputStream(OutputStream out) : 创建一个新的缓冲输出流。
1
2
3
4
// 创建字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("bis.txt"));
// 创建字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("bos.txt"));

字符缓冲流

  • public BufferedReader(Reader in) :创建一个新的缓冲输入流。
  • public BufferedWriter(Writer out) : 创建一个新的缓冲输出流。
1
2
3
4
// 创建字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("br.txt"));
// 创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("bw.txt"));

特有方法

  • BufferedReaderpublic String readLine() : 读一行文字。
  • BufferedWriterpublic void newLine() : 写一行行分隔符,由系统属性定义符号。
public String readLine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// public String readLine

public class BufferedReaderDemo {
    public static void main(String[] args) throws IOException {
         // 创建流对象
        BufferedReader br = new BufferedReader(new FileReader("in.txt"));
        // 定义字符串,保存读取的一行文字
        String line  = null;
        // 循环读取,读取到最后返回null
        while ((line = br.readLine())!=null) {
            System.out.print(line);
            System.out.println("‐‐‐‐‐‐");
        }
        // 释放资源
        br.close();
    }
}
public void newLine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// public void newLine

public class BufferedWriterDemo throws IOException {
    public static void main(String[] args) throws IOException  {
        // 创建流对象
        BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt"));
        // 写出数据
        bw.write("张三");
        // 写出换行
        bw.newLine();
        bw.write("李四");
        bw.newLine();
        bw.write("王五");
        bw.newLine();
        // 释放资源
        bw.close();
    }
}

转换流

字符编码和字符集

计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码 。比如说,按照A规则存储,同样按照A规则解析,那么就能显示正确的文本f符号。反之,按照A规则存储,再按照B规则解析,就会导致乱码现象。

  • 字符编码 Character Encoding : 就是一套自然语言的字符与二进制数之间的对应规则。
  • 字符集 Charset :也叫编码表。是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等。

常见字符集有ASCII字符集GBK字符集Unicode字符集等。

编码引出的问题

在IDEA中,使用 FileReader 读取项目中的文本文件。由于IDEA的设置,都是默认的 UTF-8 编码,所以没有任何问题。但是,当读取Windows系统中创建的文本文件时,由于Windows系统的默认是GBK编码,就会出现乱码。

InputStreamReader类

转换流 java.io.InputStreamReader ,是Reader的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。

  • InputStreamReader(InputStream in) : 创建一个使用默认字符集的字符流。
  • InputStreamReader(InputStream in, String charsetName) : 创建一个指定字符集的字符流。
1
2
InputStreamReader isr = new InputStreamReader(new FileInputStream("in.txt"));
InputStreamReader isr2 = new InputStreamReader(new FileInputStream("in.txt") , "GBK");

OutputStreamWriter类

转换流 java.io.OutputStreamWriter ,是Writer的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。

  • OutputStreamWriter(OutputStream in): 创建一个使用默认字符集的字符流。
  • OutputStreamWriter(OutputStream in, String charsetName) : 创建一个指定字符集的字符流。
1
2
OutputStreamWriter isr = new OutputStreamWriter(new FileOutputStream("out.txt"));
OutputStreamWriter isr2 = new OutputStreamWriter(new FileOutputStream("out.txt") , "GBK");

序列化流

Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该 对象的数据 、 对象的类型 和 对象中存储的属性 等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。
反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化。 对象的数据 、 对象的类型 和 对象中存储的数据 信息,都可以用来在内存中创建对象。看图理解序列化:

ObjectOutputStream类

java.io.ObjectOutputStream类,将Java对象的原始数据类型写出到文件,实现对象的持久存储。

  • public ObjectOutputStream(OutputStream out): 创建一个指定OutputStreamObjectOutputStream
1
2
FileOutputStream fileOut = new FileOutputStream("employee.txt");
ObjectOutputStream out = new ObjectOutputStream(fileOut);

序列化操作

  1. 一个对象要想序列化,必须满足两个条件:
  • 该类必须实现 java.io.Serializable 接口, Serializable 是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出 NotSerializableException
  • 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用transient 关键字修饰。
  1. 写出对象方法
    public final void writeObject (Object obj) : 将指定的对象写出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SerializeDemo {
public static void main(String[] args) {
Employee e = new Employee();
e.name = "zhangsan";
e.address = "beiqinglu";
e.age = 20;
try {
// 创建序列化流对象
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("employee.txt"));
// 写出对象
out.writeObject(e);
// 释放资源
out.close();
// 姓名,地址被序列化,年龄没有被序列化。
System.out.println("Serialized data is saved");
} catch (IOException i) {
i.printStackTrace();
}
}
}

ObjectInputStream类

ObjectInputStream反序列化流,将之前使用ObjectOutputStream序列化的原始数据恢复为对象。

  • public ObjectInputStream(InputStream in) : 创建一个指定InputStreamObjectInputStream
  • public final Object readObject () : 读取一个对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;

public class SerializeDemo2 {
public static void main(String[] args) {
Employee e = null;
try {
// 创建反序列化流
FileInputStream fileIn = new FileInputStream("employee.txt");
ObjectInputStream in = new ObjectInputStream(fileIn);
// 读取一个对象
e = (Employee) in.readObject();
// 释放资源
in.close();
fileIn.close();
} catch (IOException i) {
// 捕获其他异常
i.printStackTrace();
return;
} catch (ClassNotFoundException c) {
// 捕获类找不到异常
System.out.println("Employee class not found");
c.printStackTrace();
return;
}
// 无异常,直接打印输出
System.out.println("Name: " + e.name);
System.out.println("Address: " + e.address);
System.out.println("age: " + e.age);
}
}

Name: zhangsan
Address: beiqinglu
age: 0

age为瞬态, 未被序列化

对于JVM可以反序列化对象,它必须是能够找到class文件的类。如果找不到该类的class文件,则抛出一个ClassNotFoundException 异常。
另外,当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个 InvalidClassException 异常。发生这个异常的原因如下:

  • 该类的序列版本号与从流中读取的类描述符的版本号不匹配
  • 该类包含未知数据类型
  • 该类没有可访问的无参数构造方法

Serializable (标识型)接口给需要序列化的类,提供了一个序列版本号。 serialVersionUID 该版本号的目的在于验证序列化的对象和对应类是否版本匹配。
如果没有serialVersionUID, 那么类只要重新编译, 原来序列化的数据就无法反序列化了,因为每次编译都会自动随机产生一个serialVersionUID, 如果加了, 就算重新编译, 依然可以反序列化

1
2
3
4
5
6
7
8
9
10
11
12
public class Employee implements java.io.Serializable {
     // 加入序列版本号
     private static final long serialVersionUID = 1L;
     public String name;
     public String address;
     // 添加新的属性 ,重新编译, 可以反序列化,该属性赋为默认值.
     public int eid; 
 
     public void addressCheck() {
         System.out.println("Address  check : " + name + " ‐‐ " + address);
     }
}

瞬时态(transient)和静态(static)修饰的变量不会被序列化, 因为静态变量不属于某个对象, 是所有对象共享的. 而序列化是保存对象的状态信息, 指每个对象独立的信息.

打印流

平时我们在控制台打印输出,是调用 print 方法和 println 方法完成的,这两个方法都来自于java.io.PrintStream 类,该类能够方便地打印各种数据类型的值,是一种便捷的输出方式。

PrintStream类

  • public PrintStream(String fileName) : 使用指定的文件名创建一个新的打印流。

System.out 就是 PrintStream 类型的,只不过它的流向是系统规定的,打印在控制台上。

属性集

java.util.Properties 继承于 Hashtable ,来表示一个持久的属性集。它使用键值结构存储数据,每个键及其对应值都是一个字符串。该类也被许多Java类使用,比如获取系统属性时, System.getProperties 方法就是返回一个 Properties 对象。

Properties类

  • public Properties(): 创建一个空的属性列表
  • public Object setProperty(String key, String value) : 保存一对属性。
  • public String getProperty(String key) :使用此属性列表中指定的键搜索属性值。
  • public Set<String> stringPropertyNames() :所有键的名称的集合。
  • public void load(InputStream inStream) : 从字节输入流中读取键值对。

文本中的数据,必须是键值对形式,可以使用空格、等号、冒号等符号分隔。

网络编程

三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。

第一次握手,客户端向服务器端发出连接请求,等待服务器确认。
第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。
第三次握手,客户端再次向服务器端发送确认信息,确认连接。

TCP

  1. 客户端: java.net.Socket 类表示。创建 Socket 对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信。
  2. 服务端: java.net.ServerSocket 类表示。创建 ServerSocket 对象,相当于开启一个服务,并等待客户端的连接。

Socket类

  • public Socket(String host, int port): 创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的hostnull,则相当于指定地址为回送地址。
  • public InputStream getInputStream() : 返回此套接字的输入流。

    如果此Scoket具有相关联的通道,则生成的InputStream 的所有操作也关联该通道。关闭生成的InputStream也将关闭相关的Socket

  • public OutputStream getOutputStream() : 返回此套接字的输出流。

    如果此Scoket具有相关联的通道,则生成的OutputStream 的所有操作也关联该通道。关闭生成的OutputStream也将关闭相关的Socket

  • public void close() :关闭此套接字。

    一旦一个socket被关闭,它不可再使用。关闭此socket也将关闭相关的InputStreamOutputStream

  • public void shutdownOutput() : 禁用此套接字的输出流。

    任何先前写出的数据将被发送,随后终止输出流。

ServerSocket类

  • public ServerSocket(int port) :使用该构造方法在创建ServerSocket对象时,就可以将其绑定到一个指定的端口号上,参数port就是端口号。
  • public Socket accept() :侦听并接受连接,返回一个新的Socket对象,用于和客户端实现通信。该方法会一直阻塞直到建立连接。

流程

  1. 【服务端】启动,创建ServerSocket对象,等待连接。
  2. 【客户端】启动,创建Socket对象,请求连接。
  3. 【服务端】接收连接,调用accept方法,并返回一个Socket对象。
  4. 【客户端】Socket对象,获取OutputStream,向服务端写出数据。
  5. 【服务端】Scoket对象,获取InputStream,读取客户端发送的数据。
  6. 【服务端】Socket对象,获取OutputStream,向客户端回写数据。
  7. 【客户端】Scoket对象,获取InputStream,解析回写数据。
  8. 【客户端】释放资源,断开连接。
服务端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//服务端
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;


public class ServerDemo {

public static void main(String[] args) throws IOException {
System.out.println("服务端启动, 等待连接......");
ServerSocket serverSocket = new ServerSocket(9999);

Socket accept = serverSocket.accept();

InputStream inputStream = accept.getInputStream();
System.out.println("accept = " + accept);

byte[] bytes = new byte[1024];
int read = inputStream.read(bytes);
System.out.println(new String(bytes));

inputStream.close();
serverSocket.close();

System.out.println("服务端关闭......");

}
}
客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//客户端
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

public class ClientDemo {
public static void main(String[] args) throws IOException {
System.out.println("客户端启动");
Socket socket = new Socket("127.0.0.1", Integer.parseInt("9999"));

OutputStream os = socket.getOutputStream();
os.write("hello".getBytes());

os.close();
socket.close();

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//初始化时已进行 connect
private Socket(SocketAddress address, SocketAddress localAddr,
boolean stream) throws IOException {
setImpl();

// backward compatibility
if (address == null)
throw new NullPointerException();

try {
createImpl(stream);
if (localAddr != null)
bind(localAddr);
connect(address);
} catch (IOException | IllegalArgumentException | SecurityException e) {
try {
close();
} catch (IOException ce) {
e.addSuppressed(ce);
}
throw e;
}
}

UDP

jdk8

函数式接口

函数式接口在Java中是指:有且仅有一个抽象方法的接口

函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。

“语法糖”是指使用更加方便,但是原理不变的代码语法。例如在遍历集合时使用的for-each语法,其实底层的实现原理仍然是迭代器,这便是“语法糖”。从应用层面来讲,Java中的Lambda可以被当做是匿名内部类的“语法糖”,但是二者在原理上是不同的。

1
2
3
4
修饰符 interface 接口名称 {
    public abstract 返回值类型 方法名称(可选参数信息);
    // 其他非抽象方法内容
}

由于接口当中抽象方法的 public abstract 是可以省略的,所以定义一个函数式接口很简单:

1
2
3
public interface MyFunctionalInterface {    
    void myMethod();
}

@FunctionalInterface注解

@Override注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。该注解可用于一个接口的定义上:

1
2
3
4
@FunctionalInterface
public interface MyFunctionalInterface {
    void myMethod();
}

一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。需要注意的是,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。

自定义函数式接口

1
2
3
4
5
6
7
8
9
10
11
public class Demo09FunctionalInterface {    
    // 使用自定义的函数式接口作为方法参数
    private static void doSomething(MyFunctionalInterface inter) {
        inter.myMethod(); // 调用自定义的函数式接口方法
    }
    
    public static void main(String[] args) {
        // 调用使用函数式接口的方法
        doSomething(() ‐> System.out.println("Lambda执行啦!"));
    }
}

函数式编程

Lambda的延迟执行

有些场景的代码执行后,结果不一定会被使用,从而造成性能浪费。而Lambda表达式是延迟执行的,这正好可以作为解决方案,提升性能。

性能浪费的日志案例

日志可以帮助我们快速的定位问题,记录程序运行过程中的情况,以便项目的监控和优化。
一种典型的场景就是对参数进行有条件使用,例如对日志消息进行拼接后,在满足条件的情况下进行打印输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Demo01Logger {
    private static void log(int level, String msg) {
        if (level == 1) {
            System.out.println(msg);
        }
    }
 
    public static void main(String[] args) {
        String msgA = "Hello";
        String msgB = "World";
        String msgC = "Java";
 
        log(1, msgA + msgB + msgC);
    }
}

这段代码存在问题:无论级别是否满足要求,作为 log 方法的第二个参数,三个字符串一定会首先被拼接并传入方法内,然后才会进行级别判断。如果级别不符合要求,那么字符串的拼接操作就白做了,存在性能浪费。

备注:SLF4J是应用非常广泛的日志框架,它在记录日志时为了解决这种性能浪费的问题,并不推荐首先进行字符串的拼接,而是将字符串的若干部分作为可变参数传入方法中,仅在日志级别满足要求的情况下才会进行字符串拼接。例如: LOGGER.debug("变量{}的取值为{}。", "os", "macOS") ,其中的大括号 {} 为占位符。如果满足日志级别要求,则会将“os”和“macOS”两个字符串依次拼接到大括号的位置;否则不会进行字符串拼接。这也是一种可行解决方案,但Lambda可以做到更好。

Lambda的更优写法

1
2
3
4
@FunctionalInterface
public interface MessageBuilder {  
    String buildMessage();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Demo02LoggerLambda {
    private static void log(int level, MessageBuilder builder) {
        if (level == 1) {
            System.out.println(builder.buildMessage());
        }
    }

    public static void main(String[] args) {
        String msgA = "Hello";
        String msgB = "World";
        String msgC = "Java";
 
        log(1, () ‐> msgA + msgB + msgC );
    }
}

证明Lambda的延迟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Demo03LoggerDelay {
    private static void log(int level, MessageBuilder builder) {
        if (level == 1) {
            System.out.println(builder.buildMessage());
        }
    }
 
    public static void main(String[] args) {
        String msgA = "Hello";
        String msgB = "World";
        String msgC = "Java";
 
        log(2, () ‐> {
            System.out.println("Lambda执行!");
            return msgA + msgB + msgC;
        });
    }
}

从结果中可以看出,在不符合级别要求的情况下,Lambda将不会执行。从而达到节省性能的效果。

实际上使用内部类也可以达到同样的效果,只是将代码操作延迟到了另外一个对象当中通过调用方法来完成。而是否调用其所在方法是在条件判断之后才执行的。

使用Lambda作为参数和返回值

如果方法的参数是一个函数式接口类型,那么就可以使用Lambda表达式进行替代。使用Lambda表达式作为方法参数,其实就是使用函数式接口作为方法参数。

java.lang.Runnable 接口就是一个函数式接口,假设有一个 startThread 方法使用该接口作为参数,那么就可以使用Lambda进行传参。这种情况其实和 Thread 类的构造方法参数为 Runnable 没有本质区别。

1
2
3
4
5
6
7
8
9
public class Demo04Runnable {
    private static void startThread(Runnable task) {
        new Thread(task).start();
    }
 
    public static void main(String[] args) {
        startThread(() ‐> System.out.println("线程任务执行!"));
    }
}

如果一个方法的返回值类型是一个函数式接口,那么就可以直接返回一个Lambda表达式。当需要通过一
个方法来获取一个 java.util.Comparator 接口类型的对象作为排序器时,就可以调该方法获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.Arrays;
import java.util.Comparator;
 
public class Demo06Comparator {
    private static Comparator<String> newComparator() {
        return (a, b) ‐> b.length() ‐ a.length();
    }
 
    public static void main(String[] args) {
        String[] array = { "abc""ab""abcd" };
        System.out.println(Arrays.toString(array));
        Arrays.sort(array, newComparator());
        System.out.println(Arrays.toString(array));
    }
}

直接return一个Lambda表达式即可

常用函数式接口

JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景,它们主要在 java.util.function 包中被提供。

Supplier接口

java.util.function.Supplier<T>接口仅包含一个无参的方法:T get() 。用来获取一个泛型参数指定类型的对象数据。由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.function.Supplier;
 
public class Demo08Supplier {
    private static String getString(Supplier<String> function) {
        return function.get();
    }
 
    public static void main(String[] args) {
        String msgA = "Hello";
        String msgB = "World";
        System.out.println(getString(() ‐> msgA + msgB));
    }
}

使用 Supplier 接口作为方法参数类型,通过Lambda表达式求出int数组中的最大值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Demo02Test {
    //定一个方法,方法的参数传递Supplier,泛型使用Integer
    public static int getMax(Supplier<Integer> sup){
        return sup.get();
    }
 
    public static void main(String[] args) {
        int arr[] = {2,3,4,52,333,23};
 
        //调用getMax方法,参数传递Lambda
        int maxNum = getMax(()‐>{
           //计算数组的最大值
           int max = arr[0];
           for(int i : arr){
               if(i>max){
                   max = i;
               }
           }
           return max;
        });
        System.out.println(maxNum);
    }
}

Consumer接口

java.util.function.Consumer<T> 接口则正好与Supplier接口相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型决定。

抽象方法:accept

Consumer 接口中包含抽象方法 void accept(T t),意为消费一个指定泛型的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.function.Consumer;

public class ConsumerDemo {
private static void consumeString(String name, Consumer<String> function) {
function.accept(name);
}

public static void main(String[] args) {
consumeString("张三", (String name)->{
System.out.println("name = " + name);

System.out.println("new = " + new StringBuilder(name).reverse().toString());
});
}
}

默认方法:andThen

如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是 Consumer 接口中的default方法 andThen 。下面是JDK的源代码:

1
2
3
4
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}

java.util.ObjectsrequireNonNull 静态方法将会在参数为null时主动抛出NullPointerException 异常。这省去了重复编写if语句和抛出空指针异常的麻烦。

要想实现组合,需要两个多个Lambda表达式即可,而 andThen 的语义正是“一步接一步”操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.function.Consumer;


public class ConsumerDemo2 {
private static void consumeString(Consumer<String> one, Consumer<String> two) {
one.andThen(two).accept("Hello");
}

public static void main(String[] args) {
consumeString(
s -> System.out.println(s.toUpperCase()),
s -> System.out.println(s.toLowerCase()));
}
}

格式化打印信息
下面的字符串数组当中存有多条信息,请按照格式“姓名:XX。性别:XX. ”的格式将信息打印出来。要求将打印姓名的动作作为第一个 Consumer 接口的Lambda实例,将打印性别的动作作为第二个 Consumer 接口的Lambda实例,将两个 Consumer 接口按照顺序“拼接”到一起。

1
2
3
public static void main(String[] args) {
    String[] array = { "迪丽热巴,女""古力娜扎,女""马尔扎哈,男" };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.function.Consumer;

public class DemoConsumer {
public static void main(String[] args) {
String[] array = {"迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男"};
printInfo(s -> System.out.print("姓名:" + s.split(",")[0]),
s -> System.out.println("。性别:" + s.split(",")[1] + "。"),
array);
}

private static void printInfo(Consumer<String> one, Consumer<String> two, String[] array) {
for (String info : array) {
one.andThen(two).accept(info); // 姓名:迪丽热巴。性别:女。
}
}
}


姓名:迪丽热巴。性别:女。
姓名:古力娜扎。性别:女。
姓名:马尔扎哈。性别:男。

Predicate接口

有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用
java.util.function.Predicate<T> 接口。

抽象方法:test

Predicate 接口中包含一个抽象方法: boolean test(T t) 。用于条件判断的场景:

1
2
3
4
5
6
7
8
9
10
11
12
import java.util.function.Predicate;

public class PredicateDemo {
private static void method(Predicate<String> predicate) {
boolean veryLong = predicate.test("HelloWorld");
System.out.println("字符串很长吗:" + veryLong);
}

public static void main(String[] args) {
method(s -> s.length() > 5);
}
}

默认方法:and

既然是条件判断,就会存在三种常见的逻辑关系。其中将两个 Predicate 条件使用“”逻辑连接起来实现“并且”的效果时,可以使用default方法and。其JDK源码为:

1
2
3
4
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}

如果要判断一个字符串既要包含大写“H”,又要包含大写“W”,那么:

1
2
3
4
5
6
7
8
9
10
11
12
import java.util.function.Predicate;
 
public class Demo16PredicateAnd {
    private static void method(Predicate<String> one, Predicate<String> two) {
        boolean isValid = one.and(two).test("Helloworld");
        System.out.println("字符串符合要求吗:" + isValid);
    }
 
    public static void main(String[] args) {
        method(s ‐> s.contains("H"), s ‐> s.contains("W"));
    }
}

默认方法:or

and 的“”类似,默认方法 or 实现逻辑关系中的“”。JDK源码为:

1
2
3
4
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}

如果希望实现逻辑“字符串包含大写H或者包含大写W”,那么代码只需要将“and”修改为“or”名称即可,其他都不变:

1
2
3
4
5
6
7
8
9
10
11
12
import java.util.function.Predicate;
 
public class Demo16PredicateAnd {
    private static void method(Predicate<String> one, Predicate<String> two) {
        boolean isValid = one.or(two).test("Helloworld");
        System.out.println("字符串符合要求吗:" + isValid);
    }
 
    public static void main(String[] args) {
        method(s ‐> s.contains("H"), s ‐> s.contains("W"));
    }
}

默认方法:negate

”、“”已经了解了,剩下的“(取反)也会简单。默认方法 negate 的JDK源代码为:

1
2
3
default Predicate<T> negate() {
return (t) -> !test(t);
}

从实现中很容易看出,它是执行了test方法之后,对结果boolean值进行“!”取反而已。一定要在 test 方法调用之前调用 negate 方法,正如 andor 方法一样:

1
2
3
4
5
6
7
8
9
10
11
12
import java.util.function.Predicate;
 
public class Demo17PredicateNegate {
    private static void method(Predicate<String> predicate) {
        boolean veryLong = predicate.negate().test("HelloWorld");
        System.out.println("字符串很长吗:" + veryLong);
    }
 
    public static void main(String[] args) {
        method(s ‐> s.length() < 5);
    }
}

Function接口

java.util.function.Function<T,R> 接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。

抽象方法:apply

Function 接口中最主要的抽象方法为: R apply(T t) ,根据类型T的参数获取类型R的结果使用的场景例如:将 String 类型转换为 Integer 类型。

1
2
3
4
5
6
7
8
9
10
11
12
import java.util.function.Function;

public class FunctionDemo {
private static void method(Function<String, Integer> function) {
int num = function.apply("10");
System.out.println(num + 20);
}

public static void main(String[] args) {
method(s -> Integer.parseInt(s)*2);
}
}

默认方法:andThen

Function 接口中有一个默认的 andThen 方法,用来进行组合操作。JDK源代码如:

1
2
3
4
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}

该方法同样用于“先做什么,再做什么”的场景,和 Consumer 中的 andThen 差不多:

1
2
3
4
5
6
7
8
9
10
11
12
import java.util.function.Function;
 
public class Demo12FunctionAndThen {
    private static void method(Function<String, Integer> one, Function<Integer, Integer> two) {
        int num = one.andThen(two).apply("10");
        System.out.println(num + 20);
    }
 
    public static void main(String[] args) {
        method(str‐>Integer.parseInt(str)+10, i ‐> i *= 10);
    }
}

第一个操作是将字符串解析成为int数字,第二个操作是乘以10。两个操作通过 andThen 按照前后顺序组合到了一起。

Function的前置条件泛型和后置条件泛型可以相同。

Stream流

Java 8的Lambda让我们可以更加专注于做什么(What),而不是怎么做(How)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.ArrayList;
import java.util.List;

public class StreamDemo1 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");

list.stream()
.filter(s -> s.startsWith("张"))
.filter(s -> s.length() == 3)
.forEach(System.out::println);
}
}

直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:获取流过滤姓张过滤长度为3逐一打印。代码中并没有体现使用线性循环或是其他任何算法进行遍历,我们真正要做的事情内容被更好地体现在代码中。

这张图中展示了过滤、映射、跳过、计数等多步操作,这是一种集合元素的处理方案,而方案就是一种“函数模型”。图中的每一个方框都是一个“流”,调用指定的方法,可以从一个流模型转换为另一个流模型。而最右侧的数字3是最终结果。

这里的 filtermapskip 都是在对函数模型进行操作,集合元素并没有真正被处理。只有当终结方法 count 执行的时候,整个模型才会按照指定策略执行操作。而这得益于Lambda的延迟执行特性。

“Stream流”其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,其本身并不存储任何元素(或其地址值)。

Stream(流)是一个来自数据源的元素队列

  • 元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。
  • 数据源 流的来源。 可以是集合,数组 等。

和以前的Collection操作不同, Stream操作还有两个基础的特征:

  • Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
  • 内部迭代: 以前对集合遍历都是通过Iterator或者增强for的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式,流可以直接调用遍历方法。

当使用一个流的时候,通常包括三个基本步骤:获取一个数据源(source)数据转换执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。

获取流

java.util.stream.Stream<T> 是Java 8新加入的最常用的流接口。
获取一个流非常简单,有以下几种常用的方式:

  • 所有的 Collection 集合都可以通过 stream 默认方法获取流;
  • Stream 接口的静态方法 of 可以获取数组对应的流。

根据Collection获取流

java.util.Collection 接口中加入了default方法 stream 用来获取流,所以其所有实现类均可获取流。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.*;
import java.util.stream.Stream;
 
public class Demo04GetStream {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        // ...
        Stream<String> stream1 = list.stream();
 
        Set<String> set = new HashSet<>();
        // ...
        Stream<String> stream2 = set.stream();
 
        Vector<String> vector = new Vector<>();
        // ...
        Stream<String> stream3 = vector.stream();
    }
}

根据Map获取流

java.util.Map 接口不是 Collection 的子接口,且其K-V数据结构不符合流元素的单一特征,所以获取对应的流需要分keyvalueentry等情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
 
public class Demo05GetStream {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        // ...
        Stream<String> keyStream = map.keySet().stream();
        Stream<String> valueStream = map.values().stream();
        Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream();
    }
}

根据数组获取流

1
2
3
4
5
6
7
8
9
import java.util.stream.Stream;

public class StreamDemo2 {
public static void main(String[] args) {
String[] arr = {"赵", "钱", "孙", "李"};
Stream<String> arr1 = Stream.of(arr);
arr1.forEach(System.out::println);
}
}

of 方法的参数其实是一个可变参数,所以支持数组。

1
2
3
public static<T> Stream<T> of(T... values) {
return Arrays.stream(values);
}

常用方法

流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:

  • 延迟方法:返回值类型仍然是 Stream 接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为延迟方法。)
  • 终结方法:返回值类型不再是 Stream 接口自身类型的方法,因此不再支持类似 StringBuilder 那样的链式调用。

forEach

1
void forEach(Consumer<? super T> action);

该方法接收一个 Consumer 接口函数,会将每一个流元素交给该函数进行处理。

1
2
java.util.function.Consumer<T>接口是一个消费型接口。
Consumer接口中包含抽象方法void accept(T t),意为消费一个指定泛型的数据。

filter

1
Stream<T> filter(Predicate<? super T> predicate);

该接口接收一个 Predicate 函数式接口参数(可以是一个Lambda方法引用)作为筛选条件。

1
2
3
4
java.util.stream.Predicate 函数式接口,其中唯一的抽象方法为
boolean test(T t);

该方法将会产生一个boolean值结果,代表指定的条件是否满足。如果结果为true,那么Stream流的 filter 方法将会留用元素;如果结果为false,那么 filter 方法将会舍弃元素。

map

1
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

该接口需要一个 Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。

1
2
3
4
java.util.stream.Function 函数式接口,其中唯一的抽象方法为
apply(T t);

可以将一种T类型转换成为R类型,而这种转换的动作,就称为“映射”。

count

流提供 count 方法来数一数其中的元素个数:

1
long count();

limit

limit方法可以对流进行截取,只取用前n个。

1
Stream<T> limit(long maxSize);

skip

如果希望跳过前几个元素,可以使用 skip 方法获取一个截取之后的新流:

1
Stream<T> skip(long n);

如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。

concat

如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat

1
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)

这是一个静态方法,与 java.lang.String 当中的 concat 方法是不同的。

方法引用

  • Lambda表达式写法: s -> System.out.println(s)
  • 方法引用写法: System.out::println

通过对象名引用成员方法

对象存在, 方法也存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MethodRefObject {
void printUpperCase(String str){
System.out.println("str.toUpperCase() = " + str.toUpperCase());
}
}


@FunctionalInterface
public interface Printable {
void print(String str) ;
}


public class StreamDemo3 {
static void printString(Printable pt){
pt.print("hello");
}

public static void main(String[] args) {
MethodRefObject refObject = new MethodRefObject();
printString(s->refObject.printUpperCase(s));
printString(refObject::printUpperCase);
}
}

通过类名称引用静态方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@FunctionalInterface
public interface Calcable {
    int calc(int num);
}

public class Demo06MethodRef {
    private static void method(int num, Calcable lambda) {
        System.out.println(lambda.calc(num));
    }
 
    public static void main(String[] args) {
        method(‐10, Math::abs);
    }
}

通过super引用成员方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Man extends Human {
    @Override
    public void sayHello() {
        System.out.println("大家好,我是Man!");
    }
 
    //定义方法method,参数传递Greetable接口
    public void method(Greetable g){
        g.greet();
    }
 
    public void show(){
        method(super::sayHello);
    }
}

通过this引用成员方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@FunctionalInterface
public interface Richable {
    void buy();
}

public class Husband {
    private void buyHouse() {
        System.out.println("买套房子");
    }
 
    private void marry(Richable lambda) {
        lambda.buy();
    }
 
    public void beHappy() {
        marry(this::buyHouse);
    }
}

类的构造器引用

由于构造器的名称与类名完全一样,并不固定。所以构造器引用使用 类名称::new 的格式表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Person {
    private String name;
 
    public Person(String name) {
        this.name = name;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
}

1
2
3
public interface PersonBuilder {
    Person buildPerson(String name);
}
1
2
3
4
5
6
7
8
9
public class Demo10ConstructorRef {
    public static void printName(String name, PersonBuilder builder) {
        System.out.println(builder.buildPerson(name).getName());
    }
 
    public static void main(String[] args) {
        printName("赵丽颖", Person::new);
    }
}

数组的构造器引用

数组也是 Object 的子类对象,所以同样具有构造器

1
2
3
4
@FunctionalInterface
public interface ArrayBuilder {
    int[] buildArray(int length);
}
1
2
3
4
5
6
7
8
9
public class Demo12ArrayInitRef {
    private static int[] initArray(int length, ArrayBuilder builder) {
        return builder.buildArray(length);
    }
 
    public static void main(String[] args) {
        int[] array = initArray(10int[]::new);
    }
}

反射

获取Class对象的方式

  1. Class.forName("全类名"):将字节码文件加载进内存,返回Class对象
    • 多用于配置文件,将类名定义在配置文件中。读取文件,加载类
  2. 类名.class:通过类名的属性class获取
    • 多用于参数的传递
  3. 对象.getClass():getClass()方法在Object类中定义着。
    • 多用于对象的获取字节码的方式
  • 结论:
    同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个。

Class对象功能

  1. 获取成员变量们

    • Field[] getFields() :获取所有 public 修饰的成员变量
    • Field getField(String name) 获取指定名称的 public 修饰的成员变量
    • Field[] getDeclaredFields() 获取所有的成员变量,不考虑修饰符
    • Field getDeclaredField(String name)
  2. 获取构造方法们

    • Constructor<?>[] getConstructors()
    • Constructor<T> getConstructor(类<?>... parameterTypes)
    • Constructor<T> getDeclaredConstructor(类<?>... parameterTypes)
    • Constructor<?>[] getDeclaredConstructors()
  3. 获取成员方法们:

    • Method[] getMethods()
    • Method getMethod(String name, 类<?>... parameterTypes)
    • Method[] getDeclaredMethods()
    • Method getDeclaredMethod(String name, 类<?>... parameterTypes)
  4. 获取全类名

    • String getName()

Field:成员变量

  1. 设置值

    • void set(Object obj, Object value)
  2. 获取值

    • get(Object obj)
  3. 忽略访问权限修饰符的安全检查

    • setAccessible(true):暴力反射

Constructor:构造方法

  • T newInstance(Object... initargs)
  • 如果使用空参数构造方法创建对象,操作可以简化:Class对象的newInstance方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
    public static void main(String[] args) throws Exception {
// 获取class对象
Class<Person> personClass = Person.class;
System.out.println("personClass = " + personClass);

// 获取构造方法
Constructor<Person> personConstructor = personClass.getConstructor(String.class, int.class);
System.out.println("personConstructor = " + personConstructor);

// 实例化对象
Person person = personConstructor.newInstance("张三", 20);
System.out.println("person = " + person);

System.out.println("-----------------空参数构造器----------------------");

// 获取构造方法(空参)
Constructor<Person> personConstructor1 = personClass.getConstructor();
System.out.println("personConstructor1 = " + personConstructor1);

// 实例化对象
Person person1 = personConstructor1.newInstance();
System.out.println("person1 = " + person1);

System.out.println("------------------------简化操作---------------------");
// 简化操作
Person person2 = personClass.newInstance();
System.out.println("person2 = " + person2);


personClass = class com.gao.Person
personConstructor = public com.gao.Person(java.lang.String,int)
person = Person{name='张三', age=20}
-----------------空参数构造器----------------------
personConstructor1 = public com.gao.Person()
person1 = Person{name='null', age=0}
------------------------简化操作---------------------
person2 = Person{name='null', age=0}

Method:方法对象

  • 执行方法:Object invoke(Object obj, Object... args)
  • 获取方法名称:String getName

demo

1
2
className=Person
methodName=toString
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    public static void main(String[] args) throws Exception {
// 1. 加载配置文件
// 1.1 创建Properties对象
Properties properties = new Properties();

// 1.2 加载配置文件, 转换为集合
// 1.2.1 获取配置文件
ClassLoader classLoader = RefTest.class.getClassLoader();
InputStream asStream = classLoader.getResourceAsStream("pro.properties");
properties.load(asStream);

// 2. 获取配置文件中定义的数据
String className = properties.getProperty("className");
String methodName = properties.getProperty("methodName");

// 3. 加载类进内存
Class<?> cls = Class.forName(className);

// 4. 创建对象
Constructor<?> constructor = cls.getConstructor();
Object obj = constructor.newInstance();

// 5. 获取方法对象
Method clsMethod = cls.getMethod(methodName);

// 6. 执行方法
Object invokenvokeMethod = clsMethod.invoke(obj);

System.out.println("invokenvokeMethod = " + invokenvokeMethod);

}

invokenvokeMethod = Person{name='null', age=0}

注解

注解(Annotation),也叫元数据。一种代码级别的说明。与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释.

作用分类:

① 编写文档:通过代码里标识的注解生成文档【生成文档doc文档】
② 代码分析:通过代码里标识的注解对代码进行分析【使用反射】
③ 编译检查:通过代码里标识的注解让编译器能够实现基本的编译检查【Override】

JDK中预定义的一些注解

  • @Override :检测被该注解标注的方法是否是继承自父类(接口)的
  • @Deprecated:该注解标注的内容,表示已过时
  • @SuppressWarnings:压制警告
    • 一般传递参数all @SuppressWarnings("all")

自定义注解

格式:

1
2
3
4
@元注解
【修饰符】 @interface 注解名{
配置参数列表
}
1
2
3
4
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
  • 本质:注解本质上就是一个接口,该接口默认继承Annotation接口

    • public interface MyAnno extends java.lang.annotation.Annotation {}
  • 属性:接口中的抽象方法

    • 要求:

      1. 属性的返回值类型有下列取值

        • 基本数据类型
        • String
        • 枚举
        • 注解
        • 以上类型的数组
          1
          2
          3
          4
          5
          6
          public @interface MyAnno {
          int func1() default 1;
          String func2();
          Type func3();
          MyAnno2 func4();
          }
      2. 定义了属性,在使用时需要给属性赋值

        1. 如果定义属性时,使用default关键字给属性默认初始化值,则使用注解时,可以不进行属性的赋值。
        2. 如果只有一个属性需要赋值,并且属性的名称是value,则value可以省略,直接定义值即可。
        3. 数组赋值时,值使用{}包裹。如果数组中只有一个值,则{}可以省略

元注解:用于描述注解的注解

  • @Target:描述注解能够作用的位置
    • ElementType取值:
      • TYPE:可以作用于类上
      • METHOD:可以作用于方法上
      • FIELD:可以作用于成员变量上
      • . . .
  • @Retention:描述注解被保留的阶段(SOURCE, CLASS, RUNTIME)
    • @Retention(RetentionPolicy.RUNTIME):当前被描述的注解,会保留到class字节码文件中,并被JVM读取到
  • @Documented:描述注解是否被抽取到api文档中
  • @Inherited:描述注解是否被子类继承

在程序使用(解析)注解:获取注解中定义的属性值

  1. 获取注解定义的位置的对象 (Class,Method,Field)
  2. 获取指定的注解

    • getAnnotation(Class)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      //其实就是在内存中生成了一个该注解接口的子类实现对象
      public class ProImpl implements Pro{
      public String className(){
      return "className";
      }
      public String methodName(){
      return "methodName";
      }
      }
  3. 调用注解中的抽象方法获取配置的属性值

demo

1
2
3
4
package com.anno;

public @interface MyAnno {
}

反编译结果

1
2
3
Compiled from "MyAnno.java"
public interface com.anno.MyAnno extends java.lang.annotation.Annotation {
}

替换反射

1
2
3
4
5
6
7
8
9
10
11
import	java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Retention;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ProAnno {
String className();
String methodName();
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@ProAnno(className = "Person", methodName = "toString")
public class RefTest2 {
public static void main(String[] args) throws Exception {
ProAnno proAnno = RefTest2.class.getAnnotation(ProAnno.class);
System.out.println("proAnno = " + proAnno);

String className = proAnno.className();
String methodName = proAnno.methodName();

System.out.println("className = " + className);
System.out.println("methodName = " + methodName);

Class<?> cls = Class.forName(className);
Object newInstance = cls.getConstructor(String.class, int.class).newInstance("张三", 20);

Method method = cls.getMethod(methodName);

Object o = method.invoke(newInstance);
System.out.println("o = " + o);
}
}
-------------本文结束感谢您的阅读-------------
坚持原创技术分享,您的支持将鼓励我继续创作!
0%