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的数据类型分为两大类:
- 基本数据类型:包括
整数
、浮点数
、字符
、布尔
。 - 引用数据类型:包括
类
、数组
、接口
。
基本数据类型
四类八种基本数据类型:
数据类型 | 关键字 | 内存占用 | 取值范围 |
---|---|---|---|
字节型 | byte | 1个字节 | -128~127 |
短整型 | short | 2个字节 | -32768~32767 |
整型 | int(默认) | 4个字节 | -231次方~2的31次方-1 |
长整型 | long | 8个字节 | -2的63次方~2的63次方-1 |
单精度浮点数 | float | 4个字节 | 1.4013E-45~3.4028E+38 |
双精度浮点数 | double(默认) | 8个字节 | 4.9E-324~1.7977E+308 |
字符型 | char | 2个字节 | 0-65535 |
布尔类型 | boolean | 1个字节 | true,false |
Java中的默认类型:整数类型是
int
、浮点类型是double
。
long类型:建议数据后加L
表示。
float类型:建议数据后加F
表示。
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);
}
}
类型转换
不同范围的类型进行操作时, 会自动进行类型的提升
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);
}
范围小的类型向范围大的类型提升,
byte
、short
、char
运算时直接提升为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
类来获取输入信息
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
值。
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
类。例如:
public class MyClass /*extends Object*/ {
// ...
}
toString方法
返回该对象的字符串表示, 默认是对象的类型+@+内存地址值
, python中的__str__
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中的==
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、返回对象的字符串表示形式、比较两个对象。
在比较两个对象的时候,Object
的equals
方法容易抛出空指针异常,而Objects
类中的equals
方法就优化了这个问题。方法如下:
public static boolean equals(Object a, Object b)
: 判断两个对象是否相等。
源码:
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
Date类
java.util.Date类
表示特定的瞬间,精确到毫秒。
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对象。
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点之间的毫秒差值
System.out.println(System.currentTimeMillis());
arraycopy方法
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
:将数组中指定的数据拷贝到另一个数组中。
数组的拷贝动作是系统级的,性能很高。System.arraycopy
方法具有5个参数,含义分别为:
参数序号 | 参数名称 | 参数类型 | 参数含义 |
---|---|---|---|
1 | src | Object | 源数组 |
2 | srcPos | int | 源数组索引起始位置 |
3 | dest | Object | 目标数组 |
4 | destPos | int | 目标数组索引起始位置 |
5 | length | int | 复制元素个数 |
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
容器,并将字符串添加进去。
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包中) |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
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目录中的所有的子文件或目录。
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
面向对象
访问修饰符(私有, 保护, 公开, 默认)
public | protected | default(空的) | private | |
---|---|---|---|---|
同一类中 | √ | √ | √ | √ |
同一包中(子类与无关类) | √ | √ | √ | |
不同包的子类 | √ | √ | ||
不同包中的无关类 | √ |
public
具有最大权限。private
则是最小权限。
编写代码时,如果没有特殊的考虑,建议这样使用权限:
- 成员变量使用
private
,隐藏细节。 - 构造方法使用
public
,方便创建对象。 - 成员方法使用
public
,方便调用方法。
不加权限修饰符,其访问能力与
default
修饰符相同
static关键字
类变量
当static
修饰成员变量时,该变量称为类变量。该类的每个对象都共享同一个类变量的值。任何对象都可以更改该类变量的值,但也可以在不创建该类的对象的情况下对类变量进行操作。
类变量:使用
static
关键字修饰的成员变量。
static 数据类型 变量名;
静态方法
当static
修饰成员方法时,该方法称为类方法 。静态方法在声明中有static
,建议使用类名来调用,而不需要创建类的对象。调用方式非常简单。
类方法:使用
static
关键字修饰的成员方法,习惯称为静态方法。
修饰符 static 返回值类型 方法名 (参数列表){
// 执行语句
}
静态方法调用的注意事项:
静态方法可以直接访问类变量和静态方法。
静态方法不能直接访问普通成员变量或成员方法。反之,成员方法可以直接访问类变量或静态方法。
静态方法中,不能使用this关键字。
静态方法只能访问静态成员。
调用格式
被static
修饰的成员可以并且建议通过类名直接访问。虽然也可以通过对象名访问静态成员,原因即多个对象均属于一个类,共享使用同一个静态成员,但是不建议,会出现警告信息。
// 访问类变量
类名.类变量名;
// 调用静态方法
类名.静态方法名(参数);
静态原理图解
static
修饰的内容:
- 是随着类的加载而加载的,且只加载一次。
- 存储于一块固定的内存区域(静态区),所以,可以直接被类名调用。
- 它优先于对象存在,所以,可以被所有对象共享。
静态代码块
静态代码块
:定义在成员位置,使用static修饰的代码块{ }。
- 位置:类中方法外。
- 执行:随着类的加载而执行且执行一次,优先于main方法和构造方法的执行。
public class ClassName{ static { // 执行语句 } }
作用:给类变量进行初始化赋值。用法演示,代码如下:
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
关键字
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
仅可访问非私有父类成员变量, 若想访问父类的私有成员变量, 则需要父类提供
get
和set
方法(封装原则)
子类继承父类后, 若要重写父类的方法, 则需要保证权限大于等于父类权限
返回值类型、函数名和参数列表都要一模一样。
多态
同一行为, 具有多个不同表现形式.
- 继承或者实现【二选一】
- 方法的重写【意义体现:不重写,无意义】
- 父类引用指向子类对象【格式体现】
父类类型 变量名 = new 子类对象;
变量名.方法名();
// 父类类型:指子类对象继承的父类类型,或者实现的父接口类型。
Fu f = new Zi();
f.method();
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,执行的是子类重写后方法。
定义父类:
public abstract class Animal {
public abstract void eat();
}
定义子类:
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
}
定义测试类:
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();
}
}
多态的好处
实际开发的过程中,父类类型作为方法形式参数,传递子类对象给方法,进行方法的调用,更能体现出多态的扩展性与便利。
定义父类:
public abstract class Animal {
public abstract void eat();
}
定义子类:
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
}
定义测试类:
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();
}
}
引用类型转换
多态的转型分为向上转型与向下转型两种:
向上转型
向上转型:多态本身是子类类型向父类类型向上转换的过程,这个过程是默认的。
当父类引用指向一个子类对象时,便是向上转型。
父类类型 变量名 = new 子类类型();
如:Animal a = new Cat();
向下转型
向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的。
一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型。
子类类型 变量名 = (子类类型) 父类变量名;
如:Cat c =(Cat) a;
为什么要转型
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说,不能调用
子类拥有,而父类没有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做向下转型。
定义类:
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("看家");
}
}
定义测试类:
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
}
}
转型的异常
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
关键字,给引用变量做类型的校验
变量名 instanceof 数据类型
如果变量属于该数据类型,返回true。
如果变量不属于该数据类型,返回false。
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
类似于被继承)。一个实现接口的类(可以看做是接口的子类),需要实现接口中所有的抽象方法,创建该类对象,就可以调用方法了,否则它必须是一个抽象类。
格式
public interface 接口名称 {
// 抽象方法
// 默认方法
// 静态方法
// 私有方法
}
抽象方法:使用 abstract
关键字修饰,可以省略,没有方法体。该方法供子类实现使用。
public interface InterFaceName {
public abstract void method();
}
默认方法:使用 default
修饰,不可省略,供子类调用或者子类重写。
静态方法:使用 static
修饰,供接口直接调用。
public interface InterFaceName {
public default void method() {
// 执行语句
}
public static void method2() {
// 执行语句
}
}
私有方法:使用 private
修饰,供接口中的默认方法或者静态方法调用。
public interface InterFaceName {
private void method() {
// 执行语句
}
}
基本实现
非抽象子类实现接口:
- 必须重写接口中所有抽象方法.
- 继承了接口的默认方法, 即可以直接调用, 也可以重写.
实现格式:
class 类名 implements 接口名 {
// 重写接口中抽象方法【必须】
// 重写接口中默认方法【可选】
}
抽象方法的使用: 必须全部实现
默认方法的使用: 可以继承,可以重写,二选一,但是只能通过实现类的对象来调用。
静态方法的使用: 静态与.class
文件相关,只能使用接口名调用,不可以通过实现类的类名或者实现类的对象调用
私有方法的使用 : 私有方法:只有默认方法可以调用。私有静态方法:默认方法和静态方法可以调用。
静态方法的使用
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(); //
}
}
跑起来~~~
接口的多实现
一个类可以实现多个接口
class 类名 [extends 父类名] implements 接口名1,接口名2,接口名3... {
// 重写接口中抽象方法【必须】
// 重写接口中默认方法【不重名时可选】
}
抽象方法
接口中,有多个抽象方法时,实现类必须重写所有抽象方法。如果抽象方法有重名的,只需要重写一次。
interface A {
public abstract void showA();
public abstract void show();
}
interface B {
public abstract void showB();
public abstract void show();
}
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");
}
}
默认方法
接口中,有多个默认方法时,实现类都可继承使用。如果默认方法有重名的,必须重写一次。
interface A {
public default void methodA(){}
public default void method(){}
}
interface B {
public default void methodB(){}
public default void method(){}
}
public class C implements A,B{
@Override
public void method() {
System.out.println("method");
}
}
静态方法
接口中,存在同名的静态方法并不会冲突,原因是只能通过各自接口名访问静态方法。
优先级的问题
当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的默认方法重名,子类就近选择执行父类的成员方法。
定义接口:
interface A {
public default void methodA(){
System.out.println("AAAAAAAAAAAA");
}
}
定义父类:
class D {
public void methodA(){
System.out.println("DDDDDDDDDDDD");
}
}
定义子类:
class C extends D implements A {
// 未重写methodA方法
}
定义测试类:
public class Test {
public static void main(String[] args) {
C c = new C();
c.methodA();
}
}
DDDDDDDDDDDD
接口的多继承
一个接口能继承另一个或者多个接口,这和类之间的继承比较相似。接口的继承使用 extends
关键字,子接口继承父接口的方法。如果父接口中的默认方法有重名的,那么子接口需要重写一次。
定义父接口:
interface A {
public default void method(){
System.out.println("AAAAAAAAAAAAAAAAAAA");
}
}
interface B {
public default void method(){
System.out.println("BBBBBBBBBBBBBBBBBBB");
}
}
定义子接口:
interface D extends A,B{
@Override
public default void method() {
System.out.println("DDDDDDDDDDDDDD");
}
}
子接口重写默认方法时,default关键字可以保留。
子类重写默认方法时,default关键字不可以保留。
小结
- 接口中,无法定义成员变量,但是可以定义常量,其值不可以改变,默认使用
public static final
修饰。- 接口中,没有构造方法,不能创建对象。
- 接口中,没有静态代码块
抽象类
父类中的方法,被它的子类们重写,子类各自的实现都不尽相同。那么父类的方法声明和方法主体,只有声明还有意义,而方法主体则没有存在的意义了。我们把没有方法主体的方法称为抽象方法。Java语法规定,包含抽象方法的类就是抽象类。
修饰符 abstract 返回值类型 方法名 (参数列表);
public abstract void run();
如果一个类中含有抽象方法, 那么该类必须是抽象类
public abstract class Animal {
public abstract void run();
}
使用
继承抽象类的子类必须重写父类所有的抽象方法。否则,该子类也必须声明为抽象类。最终,必须有子类实现该父类的抽象方法,否则,从最初的父类到最终的子类都不能创建对象,失去意义。
注意事项
-
抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。
理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。
-
抽象类中,可以有构造方法,是供子类创建对象时,初始化父类成员使用的。
理解:子类的构造方法中,有默认的super(),需要访问父类构造方法。
-
抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。
-
抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类。
理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。
final
用于修饰不可改变内容。
final
: 不可改变。可以用于修饰类、方法和变量。
- 类:被修饰的类,不能被继承。
- 方法:被修饰的方法,不能被重写。
- 变量:被修饰的变量,不能被重新赋值。
修饰类
final class 类名 {
}
像 public final class String
、 public final class Math
、 public final class Scanner
等,都是被final
修饰的,目的就是供我们使用,而不让我们随意改变其内容。
修饰方法
修饰符 final 返回值类型 方法名(参数列表){
//方法体
}
重写被 final
修饰的方法,编译时就会报错。
修饰变量
局部变量 - 基本类型
基本类型的局部变量,被final
修饰后,只能赋值一次,不能再更改。
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
修饰后,只能指向一个对象,地址不能再更改。但是不影响对象内部的成员变量值的修改
public class FinalDemo2 {
public static void main(String[] args) {
// 创建 User 对象
final User u = new User();
// 创建 另一个 User对象
u = new User(); // 报错,指向了新的对象,地址值改变。
// 调用setName方法
u.setName("张三"); // 可以修改
}
}
成员变量
成员变量涉及到初始化的问题,初始化方式有两种,只能二选一:
- 显示初始化
public class User {
final String USERNAME = "张三";
private int age;
}
- 构造方法初始化
public class User {
final String USERNAME ;
private int age;
public User(String username, int age) {
this.USERNAME = username;
this.age = age;
}
}
被final修饰的常量名称,一般都有书写规范,所有字母都大写。
内部类
class 外部类 {
class 内部类{
}
}
访问特点
- 内部类可以直接访问外部类的成员,包括私有成员。
- 外部类要访问内部类的成员,必须要建立内部类的对象。
外部类名.内部类名 对象名 = new 外部类型().new 内部类型();
定义类:
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;
}
}
定义测试类:
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
匿名内部类
匿名内部类 :是内部类的简化写法。它的本质是一个带具体实现的
父类或者父接口的
匿名的
子类对象。
匿名内部类必须继承一个父类或者实现一个父接口。
new 父类名或者接口名(){
// 方法重写
@Override
public void method() {
// 执行语句
}
};
定义接口:
public abstract class FlyAble{
public abstract void fly();
}
创建匿名内部类,并调用:
public class InnerDemo {
public static void main(String[] args) {
/*
1.等号右边:是匿名内部类,定义并创建该接口的子类对象
2.等号左边:是多态赋值,接口类型引用指向子类对象
*/
FlyAble f = new FlyAble(){
public void fly() {
System.out.println("我飞了~~~");
}
};
//调用 fly方法,执行重写后的方法
f.fly();
}
}
通常在方法的形式参数是接口或者抽象类时,也可以将匿名内部类作为参数传递
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();
}
}
以上两步,也可以简化为一步
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.List
和java.util.Set
。其中,List
的特点是元素有序
、元素可重复
。Set
的特点是元素无序
,而且不可重复
。List
接口的主要实现类有java.util.ArrayList
和java.util.LinkedList
,Set
接口的主要实现类有java.util.HashSet
和java.util.TreeSet
。
Collection 常用功能
Collection
是所有单列集合的父接口,因此在Collection
中定义了单列集合(List
和Set
)通用的一些方法,这些方法可用于操作所有的单列集合。方法如下:
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.Iterator
。Iterator
接口也是Java集合
中的一员,但它与Collection
、Map
接口有所不同,Collection
接口与Map
接口主要用于存储元素,而Iterator
主要用于迭代访问(即遍历
)Collection
中的元素,因此Iterator
对象也被称为迭代器
。
想要遍历Collection
集合,那么就要获取该集合迭代器完成迭代操作.
public Iterator iterator()
: 获取集合对应的迭代器,用来遍历集合中的元素的。- 迭代:即
Collection
集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续在判断,如果还有就再取出出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。
Iterator接口的常用方法:
public E next()
:返回迭代的下一个元素。public boolean hasNext()
:如果仍有元素可以迭代,则返回true
。
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
/**
* 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
迭代器,所以在遍历的过程中,不能对集合中的元素进行增删操作。
for(元素的数据类型 变量 : Collection集合or数组){
//写操作代码
}
泛型
泛型通配符
当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符<?>
表示。但是一旦使用泛型的通配符后,只能使用Object
类中的共性方法,集合中元素自身方法无法使用。
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的父类
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.HashSet
是 Set
接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的(即存取顺序不一致)。 java.util.HashSet
底层的实现其实是一个java.util.HashMap
支持
HashSet
是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取
和查找
性能。保证元素唯一性的方式依赖于: hashCode
与 equals
方法。
HashSet存储自定义类型元素
给HashSet
中存放自定义类型元素时,需要重写对象中的hashCode
和equals
方法,建立自己的比较方式,才能保证HashSet
集合中的对象唯一
IDEA自动生成的方法
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 +
'}';
}
}
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
, 它是链表和哈希表组合的一个有序
数据存储结构。
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> )
:将集合中元素按照自定义的规则排序。
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.sort
或Arrays.sort
),从而允许在排序顺序上实现精确控制。还可以使用Comparator
来控制某些数据结构(如有序set
或有序映射
)的顺序,或者为那些没有自然顺序的对象collection
提供排序。需重写compare
方法
Comparable
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;
}
}
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
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;
// }
}
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()
方法。LinkedHashMap
:HashMap
下有个子类LinkedHashMap
,存储数据采用的哈希表结构
+链表结构
。通过链表结构
可以保证元素的存取顺序一致
;通过哈希表结构
可以保证键的唯一
、不重复
,需要重写键的hashCode()
方法、equals()
方法。
Map接口中的集合都有两个泛型变量,在使用时,要为两个泛型变量赋予数据类型。两个泛型变量的数据类型可以相同,也可以不同。
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集合中获取对应的值, 不存在则返回nullpublic 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对象中的值。
// 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()
:获取异常的类型和异常描述信息(不用)。
异常的处理
throw
throw new NullPointerException("要访问的arr数组不存在");
throw new ArrayIndexOutOfBoundsException("该索引在数组中不存在,已超出范围");
Objects非空判断
java/util/Objects.java:202
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
声明异常throws
修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{ }
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
try{
编写可能会出现异常的代码
}catch(异常类型 e){
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}
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
代码块中存放的代码都是一定会被执行的。
try...catch....finally:自身需要处理异常,最终还得关闭资源。
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. 自定义一个编译期异常: 自定义类 并继承于 java.lang.Exception 。
2. 自定义一个运行时期的异常类:自定义类 并继承于 java.lang.RuntimeException 。
线程和进程
- 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
- 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
一个程序运行后至少有一个进程,一个进程中可以包含多个线程
线程调度:
- 分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。 - 抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
并发与并行
- 并发:指两个或多个事件在同一个时间段内发生。
- 并行:指两个或多个事件在同一时刻发生(同时发生)。
在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。
而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核CPU,便是多核处理器,核 越多,并行处理的程序越多,能大大的提高电脑运行的效率。
注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。
多线程
Java中通过继承Thread类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的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接口
- 定义
Runnable
接口的实现类,并重写该接口的run()
方法,该run()
方法的方法体同样是该线程的线程执行体。 - 创建
Runnable
实现类的实例,并以此实例作为Thread
的target
来创建Thread
对象,该Thread
对象才是真正的线程对象。 - 调用线程对象的
start()
方法来启动线程。
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
对象的target
,Runnable
实现类里包含的run()
方法仅作为线程执行体。而实际的线程对象依然是Thread
实例,只是该Thread
线程负责执行其target
的run()
方法。
Thread和Runnable的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程。
匿名内部类方式实现线程的创建
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引入了线程同步机制。
- 同步代码块。
- 同步方法。
- 锁机制。
同步代码块
同步代码块: synchronized
关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:
synchronized(同步锁){
需要同步操作的代码
}
同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
- 锁对象 可以是任意类型。
- 多个线程对象 要使用同一把锁。
在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着
(BLOCKED)。
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线程执行该方法的时候,其他线程只能在方法外等着。
public synchronized void method(){
可能会产生线程安全问题的代码
}
同步锁是谁?
对于非static方法,同步锁就是this。
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
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()
: 释放同步锁。
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个数字输出一个字符串
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();
}
}
- 进入
TIMED_WAITING
状态的一种常见情形是调用的sleep
方法,单独的线程也可以调用,不一定非要有协作关系 - 为了让其他线程有机会执行,可以将
Thread.sleep()
的调用放线程run()
之内。这样才能保证该线程执行过程中会睡眠 sleep
与锁无关,线程睡眠到期自动苏醒,并返回到Runnable
(可运行)状态
sleep()
中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始立刻执行。
BLOCKED(锁阻塞)
Blocked
状态在API中的介绍为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。
线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。
这是由Runnable状态进入Blocked状态。除此Waiting以及Time Waiting状态也会在某种情况下进入阻塞状态
Waiting(无限等待)
Wating
状态在API中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。
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个方法的含义如下:
- wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是通知notify在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
- notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
- notifyAll:则释放所通知对象的 wait set 上的全部线程。
哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
总结如下:
- 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
- 否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态
调用wait和notify方法需要注意的细节
- wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
- wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
- wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。
线程池
线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
合理利用线程池能够带来三个好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约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
接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
使用线程池中线程对象的步骤:
- 创建线程池对象。
- 创建Runnable接口子类对象。(task)
- 提交Runnable接口子类对象。(take task)
- 关闭线程池(一般不做)。
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表达式
() ‐> System.out.println("多线程任务执行!")
public class RunnableImpl implements Runnable {
@Override
public void run() {
System.out.println("多线程任务执行!");
}
}
Lambda标准格式
Lambda省去面向对象的条条框框,格式由3个部分组成:
一些参数, 一个箭头, 一段代码
Lambda表达式的标准格式为:
(参数类型 参数名称) ‐> { 代码语句 }
小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
->
是新引入的语法格式,代表指向动作。
大括号内的语法与传统方法体要求基本一致。
排序时使用lambda
表达式
Arrays.sort(array, (Person a, Person b) ‐> {
return a.getAge() ‐ b.getAge();
});
Lambda的使用前提
Lambda
的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:
- 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。无论是JDK内置的 Runnable 、 Comparator 接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。
- 使用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)
: 创建文件输出流以指定的名称写入文件
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
。
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
测试文件
文件复制
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
,给定要读取的文件的名称.
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个流,按照数据类型分类:
- 字节缓冲流:
BufferedInputStream
,BufferedOutputStream
- 字符缓冲流:
BufferedReader
,BufferedWriter
缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。
字节缓冲流
public BufferedInputStream(InputStream in)
:创建一个新的缓冲输入流。public BufferedOutputStream(OutputStream out)
: 创建一个新的缓冲输出流。
// 创建字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("bis.txt"));
// 创建字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("bos.txt"));
字符缓冲流
public BufferedReader(Reader in)
:创建一个新的缓冲输入流。public BufferedWriter(Writer out)
: 创建一个新的缓冲输出流。
// 创建字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("br.txt"));
// 创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("bw.txt"));
特有方法
BufferedReader
:public String readLine()
: 读一行文字。BufferedWriter
:public void newLine()
: 写一行行分隔符,由系统属性定义符号。
// 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
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)
: 创建一个指定字符集的字符流。
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)
: 创建一个指定字符集的字符流。
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)
: 创建一个指定OutputStream
的ObjectOutputStream
。
FileOutputStream fileOut = new FileOutputStream("employee.txt");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
序列化操作
- 一个对象要想序列化,必须满足两个条件:
- 该类必须实现
java.io.Serializable
接口,Serializable
是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException
。 - 该类的
所有属性必须是可序列化的
。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用transient
关键字修饰。
- 写出对象方法
public final void writeObject (Object obj)
: 将指定的对象写出。
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)
: 创建一个指定InputStream
的ObjectInputStream
。public final Object readObject ()
: 读取一个对象。
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
, 如果加了, 就算重新编译, 依然可以反序列化
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
- 客户端:
java.net.Socket
类表示。创建Socket
对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信。 - 服务端:
java.net.ServerSocket
类表示。创建ServerSocket
对象,相当于开启一个服务,并等待客户端的连接。
Socket类
public Socket(String host, int port)
: 创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的host
是null
,则相当于指定地址为回送地址。public InputStream getInputStream()
: 返回此套接字的输入流。如果此
Scoket
具有相关联的通道,则生成的InputStream
的所有操作也关联该通道。关闭生成的InputStream
也将关闭相关的Socket
。public OutputStream getOutputStream()
: 返回此套接字的输出流。如果此
Scoket
具有相关联的通道,则生成的OutputStream
的所有操作也关联该通道。关闭生成的OutputStream
也将关闭相关的Socket
。public void close()
:关闭此套接字。一旦一个
socket
被关闭,它不可再使用。关闭此socket
也将关闭相关的InputStream
和OutputStream
。public void shutdownOutput()
: 禁用此套接字的输出流。任何先前写出的数据将被发送,随后终止输出流。
ServerSocket类
public ServerSocket(int port)
:使用该构造方法在创建ServerSocket
对象时,就可以将其绑定到一个指定的端口号上,参数port
就是端口号。public Socket accept()
:侦听并接受连接,返回一个新的Socket
对象,用于和客户端实现通信。该方法会一直阻塞直到建立连接。
流程
- 【服务端】启动,创建
ServerSocket
对象,等待连接。 - 【客户端】启动,创建
Socket
对象,请求连接。 - 【服务端】接收连接,调用
accept
方法,并返回一个Socket
对象。 - 【客户端】
Socket
对象,获取OutputStream
,向服务端写出数据。 - 【服务端】
Scoket
对象,获取InputStream
,读取客户端发送的数据。 - 【服务端】
Socket
对象,获取OutputStream
,向客户端回写数据。 - 【客户端】
Scoket
对象,获取InputStream
,解析回写数据。 - 【客户端】释放资源,断开连接。
//服务端
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("服务端关闭......");
}
}
//客户端
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();
}
}
//初始化时已进行 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可以被当做是匿名内部类的“语法糖”,但是二者在原理上是不同的。
修饰符 interface 接口名称 {
public abstract 返回值类型 方法名称(可选参数信息);
// 其他非抽象方法内容
}
由于接口当中抽象方法的 public abstract 是可以省略的,所以定义一个函数式接口很简单:
public interface MyFunctionalInterface {
void myMethod();
}
@FunctionalInterface注解
与@Override
注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface
。该注解可用于一个接口的定义上:
@FunctionalInterface
public interface MyFunctionalInterface {
void myMethod();
}
一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。需要注意的是,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。
自定义函数式接口
public class Demo09FunctionalInterface {
// 使用自定义的函数式接口作为方法参数
private static void doSomething(MyFunctionalInterface inter) {
inter.myMethod(); // 调用自定义的函数式接口方法
}
public static void main(String[] args) {
// 调用使用函数式接口的方法
doSomething(() ‐> System.out.println("Lambda执行啦!"));
}
}
函数式编程
Lambda的延迟执行
有些场景的代码执行后,结果不一定会被使用,从而造成性能浪费。而Lambda表达式是延迟执行的,这正好可以作为解决方案,提升性能。
性能浪费的日志案例
日志可以帮助我们快速的定位问题,记录程序运行过程中的情况,以便项目的监控和优化。
一种典型的场景就是对参数进行有条件使用,例如对日志消息进行拼接后,在满足条件的情况下进行打印输出:
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的更优写法
@FunctionalInterface
public interface MessageBuilder {
String buildMessage();
}
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的延迟
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
没有本质区别。
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 接口类型的对象作为排序器时,就可以调该方法获取。
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
表达式需要“对外提供”一个符合泛型类型的对象数据。
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
数组中的最大值。
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)
,意为消费一个指定泛型的数据。
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的源代码:
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
java.util.Objects
的requireNonNull
静态方法将会在参数为null时主动抛出NullPointerException
异常。这省去了重复编写if语句和抛出空指针异常的麻烦。
要想实现组合,需要两个或多个Lambda
表达式即可,而 andThen
的语义正是“一步接一步”操作。
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
接口按照顺序“拼接”到一起。
public static void main(String[] args) {
String[] array = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男" };
}
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)
。用于条件判断的场景:
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源码为:
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
如果要判断一个字符串既要包含大写“H”,又要包含大写“W”,那么:
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源码为:
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
如果希望实现逻辑“字符串包含大写H或者包含大写W”,那么代码只需要将“and
”修改为“or
”名称即可,其他都不变:
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源代码为:
default Predicate<T> negate() {
return (t) -> !test(t);
}
从实现中很容易看出,它是执行了test
方法之后,对结果boolean
值进行“!
”取反而已。一定要在 test
方法调用之前调用 negate
方法,正如 and
和 or
方法一样:
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
类型。
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源代码如:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
该方法同样用于“先做什么,再做什么”的场景,和 Consumer
中的 andThen
差不多:
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)
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是最终结果。
这里的 filter
、 map
、 skip
都是在对函数模型进行操作,集合元素并没有真正被处理。只有当终结方法 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
用来获取流,所以其所有实现类均可获取流。
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
数据结构不符合流元素的单一特征,所以获取对应的流需要分key
、value
或entry
等情况:
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();
}
}
根据数组获取流
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
方法的参数其实是一个可变参数,所以支持数组。
public static<T> Stream<T> of(T... values) {
return Arrays.stream(values);
}
常用方法
流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:
- 延迟方法:返回值类型仍然是
Stream
接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为延迟方法。) - 终结方法:返回值类型不再是
Stream
接口自身类型的方法,因此不再支持类似StringBuilder
那样的链式调用。
forEach
void forEach(Consumer<? super T> action);
该方法接收一个 Consume
r 接口函数,会将每一个流元素交给该函数进行处理。
java.util.function.Consumer<T>接口是一个消费型接口。
Consumer接口中包含抽象方法void accept(T t),意为消费一个指定泛型的数据。
filter
Stream<T> filter(Predicate<? super T> predicate);
该接口接收一个 Predicate
函数式接口参数(可以是一个Lambda
或方法引用
)作为筛选条件。
java.util.stream.Predicate 函数式接口,其中唯一的抽象方法为
boolean test(T t);
该方法将会产生一个boolean值结果,代表指定的条件是否满足。如果结果为true,那么Stream流的 filter 方法将会留用元素;如果结果为false,那么 filter 方法将会舍弃元素。
map
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
该接口需要一个 Function
函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。
java.util.stream.Function 函数式接口,其中唯一的抽象方法为
R apply(T t);
可以将一种T类型转换成为R类型,而这种转换的动作,就称为“映射”。
count
流提供 count
方法来数一数其中的元素个数:
long count();
limit
limit
方法可以对流进行截取,只取用前n
个。
Stream<T> limit(long maxSize);
skip
如果希望跳过前几个元素,可以使用 skip
方法获取一个截取之后的新流:
Stream<T> skip(long n);
如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。
concat
如果有两个流,希望合并成为一个流,那么可以使用 Stream
接口的静态方法 concat
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
通过对象名引用成员方法
对象存在, 方法也存在
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);
}
}
通过类名称引用静态方法
@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引用成员方法
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引用成员方法
@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
的格式表示
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;
}
}
public interface PersonBuilder {
Person buildPerson(String name);
}
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
的子类对象,所以同样具有构造器
@FunctionalInterface
public interface ArrayBuilder {
int[] buildArray(int length);
}
public class Demo12ArrayInitRef {
private static int[] initArray(int length, ArrayBuilder builder) {
return builder.buildArray(length);
}
public static void main(String[] args) {
int[] array = initArray(10, int[]::new);
}
}
反射
获取Class对象的方式
Class.forName("全类名")
:将字节码文件加载进内存,返回Class对象- 多用于配置文件,将类名定义在配置文件中。读取文件,加载类
类名.class
:通过类名的属性class获取- 多用于参数的传递
对象.getClass()
:getClass()方法在Object类中定义着。- 多用于对象的获取字节码的方式
- 结论:
同一个字节码文件(*.class)
在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个。
Class对象功能
-
获取成员变量们
Field[] getFields()
:获取所有public
修饰的成员变量Field getField(String name)
获取指定名称的public
修饰的成员变量Field[] getDeclaredFields()
获取所有的成员变量,不考虑修饰符Field getDeclaredField(String name)
-
获取构造方法们
Constructor<?>[] getConstructors()
Constructor<T> getConstructor(类<?>... parameterTypes)
Constructor<T> getDeclaredConstructor(类<?>... parameterTypes)
Constructor<?>[] getDeclaredConstructors()
-
获取成员方法们:
Method[] getMethods()
Method getMethod(String name, 类<?>... parameterTypes)
Method[] getDeclaredMethods()
Method getDeclaredMethod(String name, 类<?>... parameterTypes)
-
获取全类名
String getName()
Field:成员变量
-
设置值
void set(Object obj, Object value)
-
获取值
get(Object obj)
-
忽略访问权限修饰符的安全检查
setAccessible(true)
:暴力反射
Constructor:构造方法
T newInstance(Object... initargs)
- 如果使用空参数构造方法创建对象,操作可以简化:
Class对象的newInstance方法
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
className=Person
methodName=toString
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")
- 一般传递参数all
自定义注解
格式:
@元注解
【修饰符】 @interface 注解名{
配置参数列表
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
-
本质:注解本质上就是一个接口,该接口默认继承
Annotation
接口public interface MyAnno extends java.lang.annotation.Annotation {}
-
属性:接口中的抽象方法
- 要求:
-
属性的返回值类型有下列取值
- 基本数据类型
- String
- 枚举
- 注解
- 以上类型的数组
public @interface MyAnno { int func1() default 1; String func2(); Type func3(); MyAnno2 func4(); }
-
定义了属性,在使用时需要给属性赋值
- 如果定义属性时,使用default关键字给属性默认初始化值,则使用注解时,可以不进行属性的赋值。
- 如果只有一个属性需要赋值,并且属性的名称是value,则value可以省略,直接定义值即可。
- 数组赋值时,值使用{}包裹。如果数组中只有一个值,则{}可以省略
-
- 要求:
元注解:用于描述注解的注解
@Target
:描述注解能够作用的位置ElementType
取值:TYPE
:可以作用于类上METHOD
:可以作用于方法上FIELD
:可以作用于成员变量上- . . .
@Retention
:描述注解被保留的阶段(SOURCE
,CLASS
,RUNTIME
)@Retention(RetentionPolicy.RUNTIME)
:当前被描述的注解,会保留到class字节码文件中,并被JVM读取到
@Documented
:描述注解是否被抽取到api文档中@Inherited
:描述注解是否被子类继承
在程序使用(解析)注解:获取注解中定义的属性值
- 获取注解定义的位置的对象 (Class,Method,Field)
- 获取指定的注解
getAnnotation(Class)
//其实就是在内存中生成了一个该注解接口的子类实现对象 public class ProImpl implements Pro{ public String className(){ return "className"; } public String methodName(){ return "methodName"; } }
- 调用注解中的抽象方法获取配置的属性值
demo
package com.anno;
public @interface MyAnno {
}
反编译结果
Compiled from "MyAnno.java"
public interface com.anno.MyAnno extends java.lang.annotation.Annotation {
}
替换反射
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();
}
@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);
}
}