基础知识#
**目标:**快速复习基础知识
面向对象思想概述:
- Java是一种面向对象的高级编程语言。
- 面向对象:是用代码去高度模拟现实世界的事物,从而让软件为任务处理业务,为人类服务。
- 高级语言:代码看起来很像人类的自然语言。
面向对象最重要的两个概念:类和对象。
- 类: 相同事物共同特征的描述。类只是学术上的一个概念并非真实存在的,只能描述一类事物。
- 对象:是真实存在的实例。实例==对象。
- 结论:有了类和对象就可以描述万千世界所有的事物。必须先有类才能有对象。
定义类:
格式:修饰符 class 类名{
}
注意:
- 1.
类名的首字母建议大写。满足驼峰模式。 StudentNameCode - 2.一个Java代码文件中可以定义多个类。但是按照规范还是建议一个Java文件定义一个类。
- 3.一个Java代码文件中,
只能有一个类是用public修饰的,而且public修饰的类名必须成为当前Java代码的文件名称。
**类中的成分:**有且仅有五大成分(五大金刚)
修饰符 class 类名{
1.成员变量(Field): 描述类或者对象的属性信息的。
2.成员方法(Method):描述类或者对象的行为信息的。
3.构造器(Constructor):初始化类的一个对象并返回引用。
4.代码块(后面学习的)
5.内部类(后面学习的)
}
类中有且仅有这五种成分,否则代码报错!
构造器:
格式:修饰符 类名(形参列表){
}
注意:构造器没有返回值
作用:初始化类的一个对象返回。
构造器的分类:无参数构造器,有参数构造器。
构造器的注意点:一个类默认自带一个无参数构造器,但是如果写了有参数构造器那么默认的无参数构造器就消失了,此时如果还需要用无参数构造器就需要自己从新写一个。
构造器初始化对象的格式:类名 对象名称 = new 构造器; Student s = new Student();
无参数构造器的作用:初始化一个类的对象(使用对象的默认值初始化)返回。
有参数构造器的作用:初始化一个类的对象(可以在初始化对象的时候为对象属性赋值)返回。
**面向对象的三大特征:**封装,继承,多态。
封装的哲学思维:合理隐藏,合理暴露。
封装最初的目的:提高代码的安全性和复用性,组件化(提供1套getter setter方法来访问)。
封装的步骤:
成员变量应该私有。用private修饰的方法,成员变量,构造器等,只能在本类中直接访问。提供成套的getter和setter方法暴露成员变量的取值和赋值方法。
this关键字的作用:
- this代表了
当前对象的引用。 - this关键字可以用在实例方法和构造器中。
- this用在方法中,
谁调用这个方法,this就代表谁。 - this用在构造器,代表了构造器正在初始化的那个对象的引用。
示例:
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
70
71
72
| package com.itheima._01知识回顾;
public class ClassDemo01 {
//类的五大成分
//1.成员变量(Field)
private String schoolName;
//2.成员方法(Method)
public String getSchoolName(){
return this.schoolName;
}
//3.构造器(Constructor)
public ClassDemo01(){
}
//4.代码块(后面学习)
{
schoolName = "黑马程序员";
}
//5.内部类(后面学习)
class Inner{
private String address;
}
System.out.println(123); //报错,因为类中有且仅有五大成分
public static void main(String[] args) {
//构造器返回类的一个对象
//无参构造器
People xiaoMing = new People();
xiaoMing.setName("小明");
xiaoMing.setAge(24);
System.out.println(xiaoMing); //包的权限名.对象的类型@(16进制地址) com.itheima._01知识回顾.People@6a5fc7f7
System.out.println(xiaoMing.getName() + ":" + xiaoMing.getAge()); //小明:24
//有参构造器 在初始化对象的时候为对象属性赋值
People xiaoHu = new People("小虎", 25);
System.out.println(xiaoHu); ////包的权限名.对象的类型@(16进制地址) com.itheima._01知识回顾.People@73f792cf
System.out.println(xiaoHu.getName() + ":" + xiaoHu.getAge()); //小虎:25
}
}
class People {
private String name;
private int age;
public People(String name, int age) {
//this代表的是正在初始化的那个对象
this.name = name;
this.age = age;
}
public People() {
}
public String getName() {
return name;
}
public void setName(String name) {
//谁调用这个方法,this就代表谁!!!
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
|
static关键字#
**目标:**static关键字的概述。(重点)
引入:
- 我们之前定义了很多成员变量(name, age)
- 其实我们只写了一份,但是发现每个对象都可以用,这就说明
- Java中这些成员变量或者方法是存在所属性的。
- 有些属性是属于对象的,有些是属于类本身的。
- Java是通过成员变量是否有static修饰来区分是类的还是属于对象的。
static == 静态 == 修饰的成员(方法和成员变量)属于类本身的。
按照有无static修饰,成员变量和方法可以分为:
成员变量:
(1)静态成员变量(类变量)
有static修饰的成员变量称为静态成员变量也叫类变量,属于类本身的,
直接用类名访问即可。
(2)实例成员变量
无static修饰的成员变量称为实例成员变量,属于类的每个对象的,
必须用类的对象来访问。
成员变量内存图:

成员方法:
(1)静态方法
有static修饰的成员方法称为静态方法也叫类方法,属于类本身的,
直接用类名访问即可。
(2)实例方法
无static修饰的成员方法称为实例方法,
属于类的每个对象的,必须用类的对象来访问。
static修饰成员方法的内存原理:

小结:
成员变量有2种
成员方法有2种:
- 有static修饰的属于类叫静态方法,直接用类名调用即可。
- 无static修饰的属于类的每个对象的叫实例方法,必须用类的对象调用。
成员变量#
**目标:**成员变量的分类和访问。
按照有无static修饰成员变量分为:
(1)静态成员变量:有static修饰,属于类本身与类一起加载一次,直接用类名访问即可。
(2)实例成员变量:无static修饰,属于类的每个对象的,必须先创建对象,再用对象来访问。
成员变量的访问语法:
静态成员变量访问:
类名.静态成员变量。
对象.静态成员变量。(不推荐)
实例成员变量的访问:
对象.实例成员变量。
小结:
静态成员变量有static修饰,属于类本身,与类加载一次,因为只有一份所以可以被类和类的对象共享访问。
注意:不建议用对象访问静态成员变量,静态成员变量直接用类名访问即可!!!
实例成员变量,无static修饰,属于类的对象的,必须先创建对象,然后用对象来访问!
**拓展:**成员变量和成员方法访问的拓展。(面试常考)
**方法:**实例方法,静态方法。
**成员变量:**实例成员变量,静态成员变量。
8种访问形式的问答:
a.实例方法是否可以直接访问实例成员变量?可以的,因为它们都属于对象。
b.实例方法是否可以直接访问静态成员变量?可以的,静态成员变量可以被共享访问。
c.实例方法是否可以直接访问实例方法? 可以的,实例方法和实例方法都属于对象。
d.实例方法是否可以直接访问静态方法?可以的,静态方法可以被共享访问!
——————————————————————–
a.静态方法是否可以直接访问实例变量?不可以的,实例变量必须用对象访问!!
b.静态方法是否可以直接访问静态变量?可以的,静态成员变量可以被共享访问。
c.静态方法是否可以直接访问实例方法? 不可以的,实例方法必须用对象访问!!
d.静态方法是否可以直接访问静态方法?可以的,静态方法可以被共享访问!!
实例:
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
| package com.itheima._02static关键字;
public class StaticDemo {
//静态成员变量(类变量)
private static String schoolName = "黑马程序员";
//实例成员变量
private String name;
private int age;
//静态方法
public static String getSchoolName(){
return StaticDemo.schoolName;
//注意:如果是在本类中调用静态成员变量或静态方法,类名可以省略不写
//return schoolName;
System.out.println(name); //错误,静态方法不能访问实例成员变量
System.out.println(getAge); //错误,静态方法不能访问实例方法
}
//实例方法
public int getAge(){
//实例方法访问静态方法
System.out.println(getSchoolName());
//实例方法访问静态成员变量
System.out.println(schoolName);
return age;
}
}
|
extends:使伸长;扩大;扩展;延长; 实际是子类在父类的基础上增强功能。
**目标:**继承(extends)的概述。
**面向对象的三大特征:**封装,继承,多态。
继承是Java中一般到特殊的关系,是一种子类到父类的关系。 例如:学生类继承了人类。猫类继承了动物类。
被继承的类称为:父类/超类。
继承父类的类称为:子类。
继承的作用:
可以提高代码的复用,相同代码可以定义在父类中。
然后子类直接继承父类,就可以直接使用父类的这些代码了。
子类更强大:子类不仅得到了父类的功能,它还有自己的功能。
继承的特点:
子类继承了一个父类,子类就可以直接得到父类的属性(成员变量)和行为(方法)了。
注意:子类无法访问父类中私有的属性和方法
继承的格式:
子类 extends 父类{
}
小结:
- 继承是子类到到父类的一种关系。
- 子类继承了一个父类,子类就可以直接得到父类的属性和行为了。
- 在Java中继承是 “is a” 的关系。Cat extends Animal:猫是一个动物。
- 在Java中,子类是更强大的,子类不仅继承了父类的功能,自己还可以定义自己的功能。
**目标:**了解子类能否继承父类的内容。
引入:
子类继承父类,子类就得到了父类的属性和行为。
但是并非所有父类的属性和行为等子类都可以继承。
子类不能继承父类的东西:
子类不能继承父类的构造器:子类有自己的构造器。(没有争议的)
有争议的观点(拓展):
子类是否可以继承父类的私有成员(私有成员变量,私有成员方法)?
– 我认为子类是可以继承父类的私有成员的,只是不能直接访问而已。
– 以后可以暴力去访问继承自父类的私有成员
子类是否可以继承父类的静态成员?
– 我认为子类是不能继承父类的静态成员的,
– 子类只是可以访问父类的静态成员,父类的静态成员只有一份可以被子类共享访问。共享并非继承。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public class ClassDemo {
public static void main(String[] args) {
Cat cat = new Cat();
System.out.println(Cat.schoolName);//黑马程序员
Cat.test();
cat.run; //报错
}
}
class Animal{
public static String schoolName = "黑马程序员";
public static void test(){}
private void run(){};
}
class Cat extends Animal{
}
|
**目标:**继承的特点
**单继承:**一个类只能继承一个直接父类。
为什么Java是单继承的?
答:反证法,假如Java可以多继承,请看如下代码:
class A{
public void test(){
System.out.println(“A”);
}
}
class B{
public void test(){
System.out.println(“B”);
}
}
class C extends A , B {
public static void main(String[] args){
C c = new C();
c.test(); // 出现了类的二义性!所以Java不能多继承!!
}
}
**多层继承:**一个类可以间接继承多个父类。(家谱)
一个类要么默认继承了Object类,要么间接继承了Object类,Object类是Java中的祖宗类!!
**目标:**继承后-成员变量的访问特点。
就近原则:
子类有找子类,子类没有找父类,父类没有就报错。

小结:
this代表了当前对象的引用,可以用于访问当前子类对象的成员变量。
super代表了父类对象的引用,可以用于访问父类中的成员变量,super.父类成员变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| public class ClassDemo {
public static void main(String[] args) {
Cat cat = new Cat();
System.out.println(cat.name);//对象喵喵
cat.show();
}
}
class Animal{
public String name = "黑马程序员";
}
class Cat extends Animal{
public String name = "对象喵喵";
public void show(){
String name = "局部喵喵";
System.out.println(name); //局部喵喵
System.out.println(this.name);//对象喵喵
System.out.println(super.name);//黑马程序员
}
}
|
**目标:**方法重写。
方法重写的概念:
子类继承了父类,子类就得到了父类的某个方法
但是子类觉得父类的这个方法不好用或者无法满足自己的需求
子类重写一个与父类申明一样的方法来覆盖父类的该方法,子类的这个方法就进行了方法重写。
方法重写的校验注解: @Override
Java建议在重写的方法上面加上一个@Override注解。
方法一旦加了这个注解,那就必须是成功重写父类的方法,否则报错!
@Override优势:可读性好,安全,优雅!!
方法重写的具体要求:
1.子类重写方法的名称和形参列表必须与父类被重写方法一样。
2.子类重写方法的返回值类型申明要么与父类一样,要么比父类方法返回值类型范围更小。(以后再了解)
3.子类重写方法的修饰符权限应该与父类被重写方法的修饰符权限相同或者更大。(以后再了解)
4.子类重写方法申明抛出的异常应该与父类被重写方法申明抛出的异常一样或者范围更小!(以后再了解)
方法重写的规范:
1.加上@Override注解。
2.建议"申明不变,重新实现"。
小结:
方法重写是子类重写一个与父类申明一样的方法覆盖父类的方法。
方法重写建议加上@Override注解。
方法重写的核心要求:方法名称形参列表必须与被重写方法一致!!
建议"申明不变,重新实现"。
静态方法和私有方法都不可以被重写。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| public class ClassDemo2 {
public static void main(String[] args) {
Man man = new Man();
man.run();
}
}
class People{
public void run(){
System.out.println("人会跑");
}
}
class Man extends People{
@Override
public void run() {
System.out.println("男人跑的很快");
}
}
|
**目标:**继承后-构造器的特点。
特点:
子类的全部构造器默认一定会先访问父类的无参数构造器,再执行子类自己的构造器。
为什么子类构造器会先调用父类构造器?
- 子类的构造器的第一行默认有一个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
| public class ExtendsDemo {
public static void main(String[] args) {
Dog jinMao = new Dog();
Dog jinMao1 = new Dog("金毛");
}
}
// 父类
class Animal{
public Animal(){
System.out.println("父类Animal的无参数构造器");
}
}
// 子类
class Dog extends Animal{
public Dog(){
// super(); //默认存在,即使不写:根据参数匹配调用父类无参数构造器。
System.out.println("子类Dog的无参数构造器");
}
public Dog(String name){
// super(); //默认存在,即使不写:根据参数匹配调用父类无参数构造器。
System.out.println("子类Dog的有参数构造器");
}
}
|
**目标:**super调用父类构造器。
特点:
- 子类的全部构造器默认一定会调用父类的无参数构造器。
- 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
33
34
35
36
37
38
| package com.itheima._03继承;
public class ClassDemo2 {
public static void main(String[] args) {
Man man = new Man("黑马",20);
man.run(); //黑马:20
}
}
class Man extends People{
public Man(String name, int age) {
super(name, age);//根据参数调用父类的构造器
}
@Override
public void run() {
System.out.println(getName() +":"+ getAge());
}
}
class People{
private String name;
private int age;
public People() {
}
public People(String name, int age) {
this.name = name;
this.age = age;
}
public void run(){
System.out.println("人会跑");
}
//getter and setter
}
|
super调用父类构造器内存图:

**总结与拓展:**this和super关键字使用总结
this代表了当前对象的引用(继承中指代子类对象):
this.子类成员变量。
this.子类成员方法。
this(…):可以根据参数匹配访问本类其他构造器。(还没有学习)
super代表了父类对象的引用(继承中指代了父类对象空间):
super.父类成员变量。
super.父类的成员方法。
super(…):可以根据参数匹配访问父类的构造器。
注意:
this(…)借用本类其他构造器。
super(…)调用父类的构造器。
this(...)和super(...)必须放在构造器的第一行,否则报错!
所以this(...)和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
| package com.itheima._03继承;
// 需求:希望如果不写学校默认就是”黑马“!
public class ClassDemo {
public static void main(String[] args) {
Cat cat = new Cat(1,"小花");
System.out.println(cat.getAge());
System.out.println(cat.getName());
System.out.println(cat.getSchoolName());
}
}
class Cat{
private int age;
private String name;
private String schoolName;
public Cat() {
}
public Cat(int age, String name) {
//借用兄弟构造器
this(age,name,"黑马程序员");
}
public Cat(int age, String name, String schoolName) {
this.age = age;
this.name = name;
this.schoolName = schoolName;
}
//getter and setter
}
|
引用类型#
**目标:**了解引用类型。
引用类型作为Java的数据类型,自然可以作为方法的参数类型和返回值类型。
除了基本数据类型都是引用数据类型了。
关注语法即可!!
基本数据类型:
整数、浮点数(小数)、字符、布尔
4类8种基本数据类型:

注意事项
1.字符串类型(String) 是引用数据类型,不是基本数据类型
2.Java中的默认类型,整数型int,浮点型double
3.小数是一个无限趋近的数,并非精确值
引用数据类型:
包括: 类,数组,接口,lambda,枚举(eumn),字符串
默认值: null
小结:
- 引用类型作为数据类型可以在一切可以使用类型的地方使用!!
抽象类#
目标:抽象类的入门概述。
引入:
父类知道子类一定要完成某个功能,但是每个子类实现的情况都不一样
而且子类都会用自己的功能了,父类的该功能就可以定义成抽象的方法。
拥有抽象方法的类必须定义成抽象类。
什么是抽象方法?
没有方法体,只有方法签名,必须用abstract修饰的方法就是抽象方法。
什么是抽象类?
拥有抽象方法的类必须定义成抽象类。
抽象类必须用abstract关键字修饰。
小结:
抽象类本身还是一个类,拥有类的五大成分,只不过是还可以有抽象方法。但正因为有抽象方法,所以无法创建对象。
抽象方法:没有方法体,只有方法签名,必须用abstract修饰的方法就是抽象方法。
抽象类:拥有抽象方法的类必须定义成抽象类,必须用abstract修饰。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| public class AbstractDemo {
public static void main(String[] args) {
Wolf wolf = new Wolf();
wolf.eat();
}
}
class Wolf extends Animal{
@Override
public void eat() {
System.out.println("狼吃肉");
}
}
// 抽象类:拥有了抽象方法的类必须定义成抽象类。抽象类必须加上abstract修饰。
abstract class Animal{
// 抽象方法:没有方法体,只有方法签名,必须加上abstract修饰。
public abstract void eat();
}
|
目标:抽象类的使用。
抽象类是为了被继承。
总结:
一个类继承了抽象类,必须重写完抽象类的全部抽象方法,否则这个类必须定义成抽象类。
因为拥有抽象方法的类必须定义成抽象类。
1
2
3
4
5
6
7
8
9
10
| abstract class Wolf extends Animal{
@Override
public void eat() {
System.out.println("狼吃肉");
}
}
abstract class Animal{
public abstract void eat();
public abstract void run();
}
|
目标:抽象类的特征。
抽象类的特征:有得有失。
有得:抽象类拥有了得到抽象方法的能力。
失去:抽象类失去了创建对象的能力。
面试题:抽象类是否有构造器,是否可以创建对象,为什么?
答:抽象类作为类一定有构造器,而且必须有构造器。
提供给子类继承后调用父类构造器使用的。
抽象类虽然有构造器,但是抽象类绝对不能创建对象。
abstract class Animal{
public abstract void run();
}
Animal a = new Animal();
a.run(); //抽象方法不能执行,因为它没有方法体,所以抽象类不能创建对象。
抽象类中可能存在抽象方法,抽象方法不能执行。
抽象在学术上本身意味着不能实例化。
抽象类除了有得有失之外,类的其他成分人家都具备!!
小结:
抽象类不能创建对象。
抽象类除了不能创建对象之外,类的其他成分它都具备!
抽象类中也可以没有抽象方法!!
抽象类存在的意义有两点:
(1)被继承,抽象类就是为了被子类继承,否则抽象类将毫无意义。(核心意义)
(2)抽象类体现的是"模板思想":部分实现,部分抽象。(拓展)
– 可以使用抽象类设计一个模板模式。
目标:抽象类的注意事项和总结
抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。
抽象类一定有而且是必须有构造器,是供子类创建对象时,初始化父类成员使用的。子类的构造器中,有默认的super(),需要访问父类构造器。
抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
抽象类的子类,必须重写抽象父类中所有的抽象方法,否则子类也必须定义成抽象类。
抽象类存在的意义是为了被子类继承,抽象类体现的是模板思想。抽象类中已经实现的是模板中确定的成员,抽象类不确定如何实现的定义成抽象方法,交给具体的子类去实现。
目标:接口的概述和定义等。(以理解和记住语法为主)
什么是接口?
接口是更加彻底的抽象,接口中全部是抽象方法和常量,没有其他成分。(JDK 1.8之前)
接口有啥用?
接口体现的是规范思想,实现接口的类必须重写完接口中全部的抽象方法。
规范 == 约束。
接口称为被实现,实现接口的类称为实现类。
定义接口的格式:
修饰符 interface 接口名称{
}
interface:定义接口的关键字。
接口中的成分研究(JDK 1.8之前):
1.抽象方法
接口中的抽象方法默认会加上public abstract修饰,所以可以省略不写。
2.常量
常量:是指有public static final修饰的成员变量,有且仅能被赋值一次,值不能改变。
常量的名称规范上要求全部大写,多个单词下划线连接。
常量修饰的public static final 可以省略不写,默认会加上。
小结:
定义接口使用的关键字:interface
接口中的成分在JDK 1.8之前只能有:常量和抽象方法。
在接口中常量的修饰符:public static final 可以省略不写,默认会加上。
在接口中抽象方法的修饰符:public abstract 可以省略不写,默认会加上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public interface InterfaceDemo {
// 2.常量
// 只有一份,在执行的过程中其值必须有,但是不能改变!
// 常量是public static final修饰
// 常量的名称建议字母全部大写,多个单词用“_”连接
// 在接口中常量可以省略public static final不写,默认会加上该三个修饰符!
//public static final String SCHOOL_NAME = "黑马";
String SCHOOL_NAME = "黑马";
// 1.抽象方法
// public abstract void run();
// 接口中的抽象方法默认会加上public abstract修饰,所以可以省略不写。
void run();
void work();
}
|
目标:接口的基本实现。
接口是用来被类实现的。
引入:
类与类是继承关系:一个类只能直接继承一个父类。
类与接口是实现关系:一个类可以实现多个接口。
实现接口的类称为“实现类”。
子类 继承 父类
实现类 实现 接口
实现类实现接口的格式:
修饰符 class 实现类名称 implements 接口1,接口2,接口3,….{
}
implements:实现的含义。
接口是可以被多实现的:一个类可以实现多个接口。
小结:
接口是用类被实现的,实现接口的类称为实现类。
实现接口的关键字是:implements。
接口是可以被类多实现的。
注意:一个类实现接口必须重写完接口中全部抽象方法,否则这个类必须定义成抽象类!!
目标:接口与接口的多继承。
引入:
类与类是单继承关系:一个类只能继承一个直接父类。
类与接口是多实现关系:一个类可以实现多个接口。
接口与接口是多继承关系:一个接口可以继承多个接口。
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
| class PingPongMan implements SportMan{
@Override
public void eat() {
}
@Override
public void rule() {
}
@Override
public void run() {
}
@Override
public void goAbroad() {
}
}
interface Food{
void eat();
}
interface Law{
void rule();
}
// 接口与接口的多继承!
interface SportMan extends Law , Food {
void run();
void goAbroad();
}
|
目标:JDK 1.8开始之后接口新增的三种方法(理解语法,属于Java自己的技术)
引入:
JDK 1.8之前接口中只能是抽象方法,常量。
JDK 1.8开始之后接口不再纯洁了。
JDK 1.8开始之后接口新增了如下三种方法。
a.默认方法(就是之前写的普通实例方法)
– 必须用default修饰,默认会public修饰
– 必须用接口的实现类的对象来调用。
b.静态方法
– 默认会public修饰
– 注意:接口的静态方法必须用接口的类名本身来调用。
c.私有方法(就是私有的实例方法): JDK 1.9才开始有的。
– 只能在本类中被其他的默认方法或者私有方法访问。
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 class AbstractDemo2 {
public static void main(String[] args) {
People people = new People();
//实现类实现的抽象方法
people.work();
//接口中的默认方法,由实现类对象调用
people.eat();
//接口中的默认方法,接口名.方法名 调用
InterfaceJDK8.run();
//接口中的常量
System.out.println(InterfaceJDK8.name);
}
}
class People implements InterfaceJDK8 {
@Override
public void work() {
System.out.println("人们每天都在工作");
}
}
interface InterfaceJDK8{
String name = "波仔";
void work();
//1.默认方法
default void eat(){
sleep();
System.out.println("人们每天都要吃饭");
}
//2.私有方法 可以在本类中被其他私有方法或者是默认方法调用
private void sleep(){
System.out.println("人们每天都要睡觉");
}
//3.静态方法
static void run(){
System.out.println("人们每天都要跑步");
}
}
|
拓展:实现多个接口的使用注意实现。(非常偏的语法,理解和了解即可)
1.如果实现了多个接口,多个接口中存在同名的静态方法并不会冲突,
原因是只能通过各自接口名访问静态方法。
2.当一个类,既继承一个父类,又实现若干个接口时,(重点)
父类中的成员方法与接口中的默认方法重名,子类就近选择执行父类的成员方法。
3.当一个类实现多个接口时,多个接口中存在同名的默认方法。
实现类必须重写这个方法。
4.接口中,没有构造器,不能创建对象。(重点)
接口是更彻底的抽象,连构造器都没有,自然不能创建对象!!
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
| public class InterfaceDemo {
public static void main(String[] args) {
//C.test(); // 报错 ,接口的静态方法,只能有接口名称调用!
A.test();
B.test();
Cat cat = new Cat();
cat.run();
// A2 a = new A2(); // 接口是更彻底的抽象,连构造器都没有,自然不能创建对象!!
}
}
/**
3.当一个类实现多个接口时,多个接口中存在同名的默认方法。
实现类必须重写这个方法。
*/
interface A2{
default void test(){}
}
interface B2{
default void test(){}
}
class C2 implements A2 , B2{
@Override
public void test() {
}
}
/**
2.当一个类,既继承一个父类,又实现若干个接口时,(重点)
父类中的成员方法与接口中的默认方法重名,子类就近选择执行父类的成员方法。
*/
interface A1{
default void run(){
System.out.println("输出干爹的run()方法~~~~");
}
}
class Animal{
public void run(){
System.out.println("输出亲爹Animal的run()方法~~~~");
}
}
class Cat extends Animal implements A1 {
}
/**
1. 如果实现了多个接口,多个接口中存在同名的静态方法并不会冲突,
原因是只能通过各自接口名访问静态方法。
*/
interface A{
static void test(){
System.out.println("A");
}
}
interface B{
static void test(){
System.out.println("B");
}
}
class C implements A , B{
}
|
代码块#
目标:代码块-静态代码块。
代码块是类的成分之一:
成员变量,方法,构造器,代码块,内部类。
代码块按照有无static修饰分为:
1.静态代码块。
2.实例代码块。
静态代码块的格式:
static {
}
静态代码块特点:
– 必须有static修饰。
– 会与类一起优先加载,且自动触发执行一次。
静态代码块作用:
– 可以在执行类的方法等操作之前先在静态代码块中进行静态资源的初始化操作。
小结:
静态代码块有static修饰,与类一起加载,自动触发执行一次。
静态代码块的作用:可以用于在静态代码块中进行静态资源的初始化操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| public class ClassDemo {
public static String schoolName;
public String name;
// 静态代码块,属于类,与类一起加载一次!
static {
System.out.println("静态代码块被执行");
schoolName = "黑马程序员";
}
// 实例代码块!属于对象!与对象一起加载!
{
System.out.println("实例代码块被执行");
name = "波仔";
}
public static void main(String[] args) {
ClassDemo classDemo = new ClassDemo();
System.out.println(classDemo.name);//波仔
}
}
|
final关键字#
目标:final关键字。
final是最终的含义。
final用于修饰:类,方法,变量。
1.final修饰类,类不能被继承了。
2.final可以修饰方法,方法就不能被重写了。
3.final修饰变量总规则:变量有且仅能被赋值一次。
拓展:final和abstract的关系? 互斥关系,不能同时修饰类或者同时修饰方法!!
目标:final修饰变量-局部变量。
final修饰变量的总规则:有且仅能被赋值一次。
变量有几种?
成员变量
– 静态成员变量:有static修饰,属于类,只加载一份。
– 实例成员变量:无static修饰,属于每个对象,与对象一起加载。
局部变量
– 只能方法中,构造器中,代码块中,for循环中,用完作用范围就消失了。
final修饰局部变量:
– 让值被固定或者说保护起来,执行的过程中防止被修改。
1
2
3
4
5
6
7
8
9
10
11
12
| public class FinalDemo02 {
public static void main(String[] args) {
final int age = 12; // 第一次赋值
// age = 20; // 第二次赋值,报错!
final double r = 3.14;
//r = 12.3;
buy(0.8);
}
public static void buy(final double rate){
//rate = 0.7; // 第二次赋值! ,报错!
}
}
|
目标:final修饰变量-静态成员变量。
final修饰变量的总规则:有且仅能被赋值一次。
final修饰静态成员变量,变量变成了常量。
常量:有public static final修饰,名称字母全部大写,多个单词用下划线连接。
拓展:
final修饰静态成员变量可以在哪些地方赋值一次:
1.定义的时候赋值一次。
2.可以在静态代码块中赋值一次。
1
2
3
4
5
6
7
8
9
10
| public class FinalDemo03 {
// 常量:有public static final修饰,名称字母全部大写,多个单词用下划线连接。
public static final String SCHOOL_NAME = "黑马";
public static final String SCHOOL_NAME1;
static{
SCHOOL_NAME1 = "黑马1";
//SCHOOL_NAME1 = "黑马2"; // 报错,第二次赋值!
}
}
|
单例模式#
目标:面试常考(单例模式)。
单例模式的含义: 单例模式,是一种常用的软件设计模式。通过单例模式可以保证系统中,
应用该模式的这个类永远只有一个实例。即一个类永远只有一个对象实例。
单例是为了节约内存,单例在有些业务场景下还必须用到!!
单例的应用场景:在实际开发中,有很多业务对象永远只需要一个,无论启动多少次
我们只需要一个对象,例如任务管理对象,只需要一个对象。节约内存和性能。
因为对象越多内存占用越大,极有可能出现内存溢出!
实现单例模式目前提供两种方式:
1.饿汉单例设计模式
在用类获取对象的时候,对象已经提前为你创建好了。
设计步骤:
a.定义一个类,把构造器私有。
b.定义一个静态变量存储一个对象。
c.提供一个返回单例对象的方法。
2.懒汉单例设计模式
在真正需要该对象的时候,才去创建一个对象(延迟加载对象)。
设计步骤:
a.定义一个类,把构造器私有。
b.定义一个静态变量存储一个对象。
c.提供一个返回单例对象的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| public class SingleInstanceDemo01 {
public static void main(String[] args) {
Singleton01 s1 = Singleton01.getInstance();
Singleton01 s2 = Singleton01.getInstance();
System.out.println(s1 == s2);//true
}
}
// 饿汉单例设计模式 (先把饭做好,及先创建好一个对象,需要的话直接调方法拿)
class Singleton01{
// b.定义一个静态变量存储一个对象( 在用类获取对象的时候,对象已经提前为你创建好了。)
private static final Singleton01 INSTANCE = new Singleton01();
// a.定义一个类,把构造器私有。
private Singleton01(){
}
// c.提供一个返回单例对象的方法。
public static Singleton01 getInstance(){
return INSTANCE;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // 懒汉单例设计模式(当调用的时候才创建对象)
class Singleton02{
// b.定义一个静态变量存储一个对象(这里不能创建对象,需要的时候才创建,这里只是一个变量用于存储对象!)
public static Singleton02 instance ;
// a.定义一个类,把构造器私有。
private Singleton02(){
}
// c.提供一个返回单例对象的方法。
public static Singleton02 getInstance(){
if(instance == null){
// 第一次来拿单例对象!需要创建一次对象,以后直接返回!!
instance = new Singleton02();
}
return instance;
}
}
|
枚举应用#
目标:枚举的概述和作用。
枚举是Java中的一种特殊类型。
枚举的作用:是为了做信息的标志和信息的分类。
定义枚举的格式:
修饰符 enum 枚举名称{
第一行必须都是罗列枚举类实例的名称。
}
枚举类的编译以后源代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public final class Season extends java.lang.Enum<Season> {
public static final Season SPRING = new Season();
public static final Season SUMMER = new Season();
public static final Season AUTUMN = new Season();
public static final Season WINTER = new Season();
public static Season[] values();
public static Season valueOf(java.lang.String);
}
|
枚举的特点:
1.枚举类是用final修饰的,枚举类不能被继承!
2.枚举类默认继承了java.lang.Enum枚举类。
3.枚举类的第一行都是常量,存储都是枚举类的对象。
4.枚举类的第一行必须是罗列枚举类的实例名称。
5.枚举类的构造器默认是私有的。
所以:★枚举类相当于是多例设计模式。
小结:
枚举类的特点:
1.枚举类是用final修饰的,枚举类不能被继承!
2.枚举类默认继承了java.lang.Enum枚举类。
3.枚举类的第一行都是常量,存储都是枚举类的对象。
4.枚举类的第一行必须是罗列枚举类的实例名称。
所以:枚举类相当于是多例设计模式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| /**
拓展:枚举的API
*/
public class EnumDemo03 {
public static void main(String[] args) {
Season03 s = Season03.SPRING;
System.out.println(s); // SPRING
System.out.println("---------------");
// 获取当前类的全部枚举实例: public static Season[] values()
Season03[] ss = Season03.values();
for(int i = 0 ; i < ss.length ; i++ ){
Season03 s1 = ss[i];
System.out.println(s1);
}
// 获取枚举对象的索引: ordinal()
Season03 s2 = Season03.AUTUMN;
System.out.println(s2.ordinal()); // 2
}
}
enum Season03{
SPRING, SUMMER, AUTUMN, WINTER;
}
|
目标:多态的入门概述。
面向对象的三大特征:封装,继承,多态。
多态的形式:
父类类型 对象名称 = new 子类构造器;
接口 对象名称 = new 实现类构造器;
父类类型的范围 > 子类类型范围的。
多态的概念:
同一个类型的对象,执行同一个行为,在不同的状态下会表现出不同的行为特征。
多态的识别技巧:
对于方法的调用:编译看左边,运行看右边(因为方法可以被重写)。
对于变量的调用:编译看左边,运行看左边。
多态的使用前提:
(1)必须存在继承或者实现关系。
(2)必须存在父类类型的变量引用子类类型的对象。
(3)需要存在方法重写。
小结:
记住多态的形式,识别,概念等语法即可!
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 PolymorphicDemo {
public static void main(String[] args) {
// 父类类型 对象名称 = new 子类构造器;
Animal dlam = new Cat();
dlam.run(); // 对于方法的调用:编译看左边,运行看右边。
System.out.println(dlam.name); // 对于变量的调用:编译看左边,运行看左边。
Animal taiDi = new Dog();
taiDi.run(); // 对于方法的调用:编译看左边,运行看右边。
System.out.println(taiDi.name); // 对于变量的调用:编译看左边,运行看左边。
}
}
class Dog extends Animal{
public String name = "🐶名称Dog";
@Override
public void run(){
System.out.println("🐕跑的贼快~~~~!");
}
}
class Cat extends Animal{
public String name = "🐱名称Cat";
@Override
public void run(){
System.out.println("🐱跑的飞快~~~~!");
}
}
class Animal{
public String name = "动物名称Animal";
public void run(){
System.out.println("动物跑!");
}
}
|
目标:多态的优劣势。
优势:
1.在多态形式下,右边对象可以实现组件化切换,业务功能也随之改变,
便于扩展和维护。可以实现类与类之间的解耦。
2.实际开发的过程中,父类类型作为方法形式参数,传递子类对象给方法,
可以传入一切子类对象进行方法的调用,更能体现出多态的扩展性与便利。
劣势:
1.多态形式下,不能直接调用子类特有的功能。编译看左边!! 左边
父类中没有子类独有的功能,所以代码在编译阶段就直接报错了!
小结:
记住以上语法!
目标:引用数据类型的自动类型转换。
在基础班学过了基本数据类型的转换。
1.小范围类型的变量或者值可以直接赋值给大范围类型的变量。
2.大范围类型的变量或者值必须强制类型转换给小范围类型的变量。

引用数据类型转换的思想是一样的:
父类类型的范围 > 子类类型的范围。
Animal Cat
引用数据类型的自动类型转换语法:
1.子类类型的对象或者变量可以自动类型转换赋值给父类类型的变量。
小结:
记住语法!
引用类型的自动类型转换并不能解决多态的劣势。
1
2
3
4
5
| // 1.引用类型的自动类型转换:小范围的对象赋值给大范围的变量
Animal a = new Cat();
class Animal{}
class Cat extends Animal{}
|
目标:引用类型强制类型转换
引用类型强制类型转换的语法:
1.父类类型的变量或者对象必须强制类型转换成子类类型的变量,否则报错!
强制类型转换的格式:
类型 变量名称 = (类型)(对象或者变量)
注意:有继承/实现关系的两个类型就可以进行强制类型转换,编译阶段一定不报错!
但是运行阶段可能出现:类型转换异常 ClassCastException
Java建议在进行强制类型转换之前先判断变量的真实类型,再强制类型转换!
变量 instanceof 类型: 判断前面的变量是否是后面的类型或者其子类类型才会返回true,
小结:
有继承/实现关系的两个类型就可以进行强制类型转换,编译阶段一定不报错!
但是运行阶段可能出现:类型转换异常 ClassCastException
Java建议在进行强制类型转换之前先判断变量的真实类型,再强制类型转换!
变量 instanceof 类型: 判断前面的变量是否是后面的类型或者其子类类型才会返回true
1
2
3
4
5
6
7
8
9
10
11
| // 2.多态下类型转换异常问题研究(重点)
Animal a1 = new Cat();
Wolf w1 = (Wolf) a1; // 编译阶段没有报错!在运行阶段出现ClassCastException类型转换成!
if(a1 instanceof Cat){
Cat c1 = (Cat) a1;
c1.catchMouse();
}else if(a1 instanceof Wolf){
Wolf w1 = (Wolf) a1;
w1.catchSheep();
}
|
内部类#
目标:内部类的概述和分类。
内部类是类的五大成分之一:成员变量,方法,构造器,代码块,内部类。
什么是内部类?
定义在一个类里面的类就是内部类。
内部类有什么用?
可以提供更好的封装性, 内部类有更多权限修饰符 , 封装性有更多的控制。
可以体现出组件的思想。
内部类的分类:
(1)静态内部类。
(2)实例内部类。(成员内部类)
(3)局部内部类。
(4)匿名内部类。(重点)
小结:
匿名内部类是我们的重点。
目标:静态内部类的研究(了解语法即可)
什么是静态内部类?
有static修饰,属于外部类本身,会加载一次。
静态内部类中的成分研究:
类有的成分它都有,静态内部类属于外部类本身,只会加载一次
所以它的特点与外部类是完全一样的,只是位置在别人里面而已。
外部类=宿主
内部类=寄生
静态内部类的访问格式:
外部类名称.内部类名称
静态内部类创建对象的格式:
外部类名称.内部类名称 对象名称 = new 外部类名称.内部类构造器;
1
| Outter.Inner in = new Outter.Inner();
|
静态内部类的访问拓展:
静态内部类中是否可以直接访问外部类的静态成员?可以的,外部类的静态成员只有一份,可以被共享!
静态内部类中是否可以直接访问外部类的实例成员?不可以的,外部类的是成员必须用外部类对象访问!!
小结:
静态内部类属于外部类本身,只会加载一次
所以它的特点与外部类是完全一样的,只是位置在别人里面而已。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| class Outter{
public static int age1 = 12;
private double salary;
// 静态内部类:有static修饰,属于外部类本身,只会加载一次
public static class Inner{
public void show() {
System.out.println(age1);//12
System.out.println(salary);//访问不到
}
}
}
public class InnerClass {
public static void main(String[] args) {
// 外部类名称.内部类名称 对象名称 = new 外部类名称.内部类构造器
Outter.Inner in = new Outter.Inner();
in.show();
}
}
|
目标:内部类_实例内部类(成员内部类)(了解语法为主)
什么是实例内部类:
无static修饰的内部类,属于外部类的每个对象的,跟着对象一起加载的。
实例内部类的成分特点:
实例内部类中不能定义静态成员,其他都可以定义。
可以定义常量。
实例内部类的访问格式:
外部类名称.内部类名称。
创建对象的格式:
外部类名称.内部类名称 对象名称 = new 外部类构造器.new 内部构造器;
1
| Outter.Inner in = new Outter().new Inner();
|
拓展:
实例内部类中是否可以直接访问外部类的静态成员?可以的,外部类的静态成员可以被共享访问!
实例内部类中是否可以访问外部类的实例成员?可以的,实例内部类属于外部类对象,可以直接访问当前外部类对象的实例成员!
实例内部类中不能定义静态成员,但可以定义常量。
小结:
实例内部类属于外部类对象,需要用外部类对象一起加载,
实例内部类可以访问外部类的全部成员!
目标:局部内部类。(几乎不用)
定义在方法中,在构造器中,代码块中,for循环中定义的内部类,就是局部内部类。
局部内部类中的成分特点:
只能定义实例成员,不能定义静态成员
可以定义常量。
小结:
局部内部类没啥用。
1
2
3
4
5
6
7
| public static void test(){
class Animal{
}
class Cat extends Animal{
}
}
|
目标:匿名内部类的概述(重点)
什么是匿名内部类?
就是一个没有名字的局部内部类。
匿名内部类目的是为了:简化代码,也是开发中常用的形式。
匿名内部类的格式:
new 类名|抽象类|接口(形参){
方法重写。
}
匿名内部类的特点:
1.匿名内部类是一个没有名字的内部类。
2.匿名内部类一旦写出来,就会立即创建一个匿名内部类的对象返回。
3.匿名内部类的对象的类型相当于是当前new的那个的类型的子类类型。
小结:
1.匿名内部类是一个没有名字的内部类。
2.匿名内部类一旦写出来,就会立即创建一个匿名内部类的对象返回。
3.匿名内部类的对象的类型相当于是当前new的那个的类型的子类类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| public class Anonymity {
public static void main(String[] args) {
// 抽象类不能创建对象,相当于创建了一个Animal的子类,并且这个子类没有名字。
Animal a2 = new Animal() {
@Override
public void run() {
System.out.println("人跑的快");
}
};
// 执行方法,编译看左边,运行看右边。
a2.go();
}
}
abstract class Animal{
public abstract void run();
public void go(){
System.out.println("开始go~~~");
}
}
|
包和权限修饰符#
目标:包和权限修饰符。
包:
分门别类的管理各种不同的技术。
企业的代码必须用包区分。便于管理技术,扩展技术,阅读技术。
定义包的格式:package 包名; 必须放在类名的最上面。一般工具已经帮我们做好了。
包名的命名规范:
一般是公司域名的倒写+技术名称:
http://www.itheima.com => com.itheima.技术名称
包名建议全部用英文,多个单词用”.“连接,必须是合法标识符,不能用关键字
注意:
相同包下的类可以直接访问。
不同包下的类必须导包,才可以使用!
导包格式:import 包名.类名;
目标:权限修饰符。
权限修饰符:有四种(private -> 缺省 -> protected - > public )
可以修饰成员变量,修饰方法,修饰构造器,内部类,不同修饰符修饰的成员能够被访问的权限将受到限制!
四种修饰符的访问权限范围:
private 缺省 protected public
本类中 √ √ √ √
本包下其他类中 X √ √ √
其他包下的子类中 X X √ √
其他包下的类中 X X X √
工具类#
Object类#
目标:常用API的学习-Object类的toString方法使用详解。
引入
包:java.lang.Object
Object类是Java中的祖宗类。
一个类要么默认继承了Object类,要么间接继承了Object类。
Object类的方法是一切子类都可以直接使用的,所以我们要学习Object类的方法。
Object类的常用方法:
(1)public String toString():
– 默认是返回当前对象在堆内存中的地址信息:
com.itheima._12Object类的详细使用.Student@735b478
– 默认的地址信息格式:类的全限名@内存地址
– 直接输出对象名称,默认会自动调用toString()方法,所以输出对象toString()调用可以省略不写
– 开发中直接输出对象,默认输出对象的地址其实是毫无意义的。
开发中输出对象变量,更多的时候是希望看到对象的内容数据而不是对象的地址信息!
所以父类toString()方法存在的意义就是为了被子类重写,以便返回对象的内容信息输出!!
小结:
toString()默认是返回当前对象在堆内存中的地址信息:
开发中输出对象变量,更多的时候是希望看到对象的内容数据而不是对象的地址信息!
所以父类toString()方法存在的意义就是为了被子类重写,重写toString可以看到对象的内容信息。
(2)public boolean equals(Object o):
– 默认是比较两个对象的地址是否相同。相同返回true,反之。
– 直接比较两个对象的地址是否相同完全可以用“==”替代equals。
所以equals存在的意义是为了被子类重写,以便程序员可以自己来定制比较规则。
– 需求:只要两个对象的内容一样,我们就认为他们是相等的。
小结:
equals存在的意义是为了被子类重写,以便程序员可以自己来定制比较规则。
目标:Objects类的使用。
Objects类与Object还是继承关系。
Objects类是从JDK 1.7开始之后才有的。
Objects的方法:
1.public static boolean equals(Object a, Object b)
– 比较两个对象的。
– 底层进行非空判断,从而可以避免空指针异常。更安全!!推荐使用!!
public static boolean equals(Object a, Object b) {
return a == b || a != null && a.equals(b);
}
2.public static boolean isNull(Object obj)
– 判断变量是否为null ,为null返回true ,反之!
1
2
3
4
5
6
7
8
| Student s1 = null;
Student s2 = new Student();
System.out.println(Objects.equals(s1 , s2)); // 可以避免空指针异常。更安全!! false
// System.out.println(s1.equals(s2)); // 空指针异常
// 询问s1是否为null 为null返回true ,反之!
System.out.println(Objects.isNull(s1));
System.out.println(s1 == null); // 可以直接用==判断也可以!
|
Date日期类#
目标:Date日期类的使用。
Java是面向对象的,会用一个类代表一个事物。
Date类在Java中代表的是系统当前此刻日期时间对象。
Date类:
包:java.util.Date
构造器:
– public Date():创建当前系统的此刻日期时间对象。
– public Date(long time):time时间毫秒值
方法:
– public long getTime():返回自 1970 年 1 月 1 日 00:00:00 GMT 以来走过的总的毫秒数。
时间记录的两种方式:
a.Date日期对象。
b.时间毫秒值:从1970-01-01 00:00:00开始走到此刻的总的毫秒值。 1s = 1000ms
小结:
Date可以代表系统当前此刻日期时间对象。
时间记录的两种方式:a.Date日期对象。b.时间毫秒值。
1
2
3
4
5
6
7
| // a.创建一个日期对象代表了系统此刻日期时间对象
Date d = new Date();
System.out.println(d); //Sun Aug 07 10:36:56 CST 2022
// b.拿当前日期对象的时间毫秒值
long time = d.getTime();
System.out.println(time);
|
目标:Date类的有参数构造器的使用。
构造器:
– public Date():创建当前系统的此刻日期时间对象。
– public Date(long time):把时间毫秒值转换成日期对象。
流程:
Date日期对象 -> getTime() -> 时间毫秒值
时间毫秒值 -> new Date(时间毫秒值) -> Date日期对象
小结:
public Date(long time):把时间毫秒值转换成日期对象。
1
2
3
4
5
6
7
8
9
10
11
12
| // 需求:问121s以后的时间是多少。
// 1.拿到此刻日期对象
Date d = new Date();
System.out.println(d);
// 2.拿到此刻日期对象的时间毫秒值 往后走 121 s
long time = d.getTime() + 121*1000;
// 3.把时间毫秒值转换成日期对象。
Date d1 = new Date(time);
System.out.println(d1);
|
目标:DateFormat简单日期格式化类的使用。
引入:
我们之前得到的Date日期对象或者时间毫秒值的时间形式
开发中并不喜欢,不符合有些时间格式的需求。
DateFormat作用:
1.可以把“日期对象”或者“时间毫秒值”格式化成我们喜欢的时间形式。(格式化时间)
2.可以把字符串的时间形式解析成日期对象。(解析字符串时间)
DateFormat是一个抽象类,不能直接使用,要使用它的子类:SimpleDateFormat
SimpleDateFormat简单日期格式化类:
包:java.text.SimpleDateFormat
构造器:public SimpleDateFormat(String pattern):指定时间的格式,创建简单日期格式化对象。
方法:
– public String format(Date date):可以把日期对象格式化成我们喜欢的时间形式,返回的是字符串!
– public String format(Object time):可以把时间毫秒值格式化成我们喜欢的时间形式,返回的是字符串!
– public Date parse(String date) throws ParseException:把字符串的时间解析成日期对象
小结:
简单日期格式化类SimpleDateFormat可以把日期对象格式化成我们喜欢的时间形式
– public String format(Date date):可以把日期对象格式化成我们喜欢的时间形式,返回的是字符串!
1
2
3
4
5
6
7
8
9
10
11
| // 需求:把此刻日期对象格式化成我们喜欢的形式。
// 1.得到此刻日期对象
Date d = new Date();
// 2.创建一个简单日期格式化对象负责格式化日期对象
// 注意:参数是之间的格式。EEE: 星期 a:上午/下午
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss EEE a");
// 3.开始调用方法格式化时间得到格式化的字符串时间形式
String rs = sdf.format(d);
System.out.println(rs); //2022年08月07日 15:24:54 周日 下午
|
拓展:简单日期格式化类SimpleDateFormat格式化时间毫秒值。
1
2
3
4
5
6
7
8
9
10
11
| // 1.问121s后的时间是多少。格式化输出。
// a.得到此刻日期对象
Date date = new Date();
// b.得到当前时间的时间毫秒值
long time = date.getTime();
time += 121 * 1000;
// c.格式化时间毫秒值
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss EEE a");
System.out.println(sdf.format(time));
|
目标:简单日期格式化类SimpleDateFormat解析字符串时间成为日期对象。
引入:
Date日期对象 -> 格式化成 -> 喜欢的字符串时间形式。
时间毫秒值 -> 格式化成 -> 喜欢的字符串时间形式。
字符串的时间形式 -> 解析成 -> Date日期对象。
开发中经常会收到字符串的时间,需要转成Date日期对象。“2018-10-11 10:10:22"字符串转换成Date日期对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // 面试题:请问 “2019-11-04 09:30:30” 往后 1天15小时,30分29s后的时间是多少
// a.定义一个字符串时间
String date = "2019-11-04 09:30:30";
// b.把字符串的时间解析成Date日期对象 。(重点)
// 1.创建一个简单日期格式化对象负责解析字符串的时间成为日期对象
// 注意:参数必须与被解析的时间的格式完全一致,否则执行报错!!
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 2.开始解析成日期对象
Date newDate = sdf.parse(date);
// c.得到日期对象的时间毫秒值 + 往后走 1天15小时,30分29s
long time = newDate.getTime() + (24L *60*60 + 15*60*60 + 30*60 + 29) * 1000;
// d.把时间毫秒值格式化成喜欢的字符串的时间形式!
System.out.println(sdf.format(time));
|
推荐:LocalDateTime#
目标:LocalDateTime的使用
Date与LocalDateTime的区别:
LocalDateTime的时间格式可读性更好
1
2
3
| Date date = new Date();
System.out.println(date); // Fri Nov 04 11:03:57 CST 2022
System.out.println(LocalDateTime.now()); // 2022-11-04T11:03:57.639
|
LocalDateTime线程安全
LocalDateTime基本使用:
常见的日期对象。
- LocalDateTime:日期加时间的日期对象,包含年月日时分秒
- LocalDate:日期类,包含年月日
- LocalTime:时间类,包含时分秒
1.获取当前时间
1
2
3
4
5
6
| LocalDate localDate = LocalDate.now();
LocalTime localTime = LocalTime.now();
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println("localDate:"+localDate);//2023-02-22
System.out.println("localTime:"+localTime);//17:25:36.590
System.out.println("localDateTime:"+localDateTime);//2023-02-22T17:25:36.590
|
2.获取当前时间的年月日时分秒
1
2
3
4
5
6
7
| LocalDateTime localDateTime = LocalDateTime.now(); // 获取当前时间
int year = localDateTime.getYear(); // 获取年份 2023
int month = localDateTime.getMonthValue(); // 获取月份 2
int day = localDateTime.getDayOfMonth(); // 获取月中的天数 22
int hour = localDateTime.getHour(); // 获取当前的小时 17
int minute = localDateTime.getMinute(); // 获取当前分钟 33
int second = localDateTime.getSecond(); // 获取当前秒数 22
|
3.给LocalDateTime赋值
1
2
| LocalDateTime of = LocalDateTime.of(2023,2,22,22,22,22);
System.out.println(of); // 输出2023-02-22T22:22:22
|
4.时间和字符串转换
1
2
3
4
5
6
7
8
9
| // 字符串转时间
String date = "2021-01-05 12:00:00";
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime dateTime = LocalDateTime.parse(date, dateTimeFormatter);
// 时间转字符串
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter dateTimeFormatter2 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String dateTime1 = now.format(dateTimeFormatter2); // 2022-11-04 11:24:54
|
5.在当前时间基础上加上对应的时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| LocalDateTime now = LocalDateTime.now(); // 当前时间
LocalDateTime now1 = now.plusYears(5); // 在当前时间加上5年
LocalDateTime now2 = now.plusMonths(5);// 在当前时间商加上5月
LocalDateTime now3 = now.plusDays(7); // 在当前时间加上7天
LocalDateTime now4 = now.plusHours(2); // 在当前时间加上2个小时
LocalDateTime now5 = now.plusMinutes(30); // 在当前时间加上30分钟
LocalDateTime now6 = now.plusSeconds(30); // 在当前时间加上30秒
LocalDateTime now1 = now.minusYears(5); // 在当前时间减上5年
LocalDateTime now2 = now.minusMonths(5);// 在当前时间上减上5月
LocalDateTime now3 = now.minusDays(7); // 在当前时间减上7天
LocalDateTime now4 = now.minusHours(2); // 在当前时间减上2个小时
LocalDateTime now5 = now.minusMinutes(30); // 在当前时间减上30分钟
LocalDateTime now6 = now.minusSeconds(30); // 在当前时间减上30秒
|
6.两个时间比较
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| LocalDateTime now = LocalDateTime.now(); // 当前时间
LocalDateTime now1 = now.plusYears(5); // 在当前时间加上5年
//给LocalDateTime 赋值
LocalDateTime of = LocalDateTime.of(2023,2,2,22,22,22);
LocalDateTime of1 = LocalDateTime.of(2023,8,5,1,1,1);
//两个时间作比较,第一个时间减去第二个时间(如果年份相同,比较月份,月份相同比较天数,以此类推)
int compareTo = now1.compareTo(now);
int compareTo1 = now.compareTo(now1);
int compareTo2 = now.compareTo(of);
int compareTo3 = now.compareTo(of1);
System.out.println(now); // 输出 2023-02-22T20:19:44.112v
System.out.println(now1); // 输出 2028-02-22T20:19:44.112
System.out.println(of); // 输出 2023-02-02T22:22:22
System.out.println(of1); // 输出 2023-08-05T01:01:01
System.out.println(compareTo); // 输出 5
System.out.println(compareTo1); // 输出 -5
System.out.println(compareTo2); // 输出 20
System.out.println(compareTo3); // 输出 -6
|
7.两个日期前后的比较与判断
1
2
3
4
5
| //判断两个时间点的前后
LocalDateTime now = LocalDateTime.now();
LocalDateTime target = LocalDateTime.of(2022, 2, 22, 22, 22, 22);
boolean isBefore = now.isBefore(target); //false
boolean isAfter = now.isBefore(target); //true
|
Calendar日历类#
目标:日历类Calendar的使用。
Calendar代表了系统此刻日期对应的日历对象。
Calendar是一个抽象类,不能直接创建对象。
Calendar日历类创建日历对象的语法:
Calendar rightNow = Calendar.getInstance();
Calendar的方法:
1.public static Calendar getInstance(): 返回一个日历类的对象。
2.public int get(int field):取日期中的某个字段信息。
3.public void set(int field,int value):修改日历的某个字段信息。
4.public void add(int field,int amount):为某个字段增加/减少指定的值
5.public final Date getTime(): 拿到此刻日期对象。
6.public long getTimeInMillis(): 拿到此刻时间毫秒值
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
| // 1.通过调用日历类的静态方法getInstance得到一个当前此刻日期对象对应的日历对象。
Calendar rightNow = Calendar.getInstance();
System.out.println(rightNow);
// 2.获取年:
int year = rightNow.get(Calendar.YEAR);
System.out.println(year);
int mm = rightNow.get(Calendar.MONTH) + 1;
System.out.println(mm);
// 3.一年中的第几天: 308
int days = rightNow.get(Calendar.DAY_OF_YEAR);
System.out.println(days);
// 4.修改日历的信息
//rightNow.set(Calendar.YEAR , 2099);
//System.out.println(rightNow.get(Calendar.YEAR));
// 5.日历可以得到此刻日期对象。
Date d = rightNow.getTime();
System.out.println(d);
// 6.此刻时间毫秒值
long time = rightNow.getTimeInMillis();
System.out.println(time);
// 7.请问701天 15小时后是哪个日期
// 让日历的一年中的第几天往后走 701天!
rightNow.add(Calendar.DAY_OF_YEAR , 701);
rightNow.add(Calendar.HOUR , 15);
long time1 = rightNow.getTimeInMillis();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss EEE a");
System.out.println(sdf.format(time1));
|
Math类#
目标:Math类的使用。
Math用于做数学运算。
Math类中的方法全部是静态方法,直接用类名调用即可。
方法:
方法名 说明
public static int abs(int a) 获取参数a的绝对值
public static double ceil(double a) 向上取整
public static double floor(double a) 向下取整
public static double pow(double a, double b) 获取a的b次幂
public static long round(double a) 四舍五入取整
public static int max(int a, int b) 返回两个int的较大值
public static int min(int a, int b) 返回两个int的较小值
1
2
3
4
5
6
7
8
9
10
11
12
| // 1.取绝对值:返回正数。
System.out.println(Math.abs(10));
System.out.println(Math.abs(-10.3));
// 2.向上取整: 5
System.out.println(Math.ceil(4.00000001)); // 5.0
// 3.向下取整:4
System.out.println(Math.floor(4.99999999)); // 4.0
// 4.求指数次方
System.out.println(Math.pow(2 , 3)); // 2^3 = 8.0
// 5.四舍五入 10
System.out.println(Math.round(4.49999)); // 4
System.out.println(Math.round(4.500001)); // 5
|
System类#
目标:System系统类的使用。
System代表当前系统。
静态方法:
1.public static void exit(int status):终止JVM虚拟机,非0是异常终止。
2.public static long currentTimeMillis():获取当前系统此刻时间毫秒值。
3.可以做数组的拷贝。
arraycopy(Object var0, int var1, Object var2, int var3, int var4);
- 参数一:原数组
- 参数二:从原数组的哪个位置开始赋值
- 参数三:目标数组
- 参数四:赋值到目标数组的哪个位置
- 参数五:赋值几个
BigDecimal计算浮点型#
目标:BigDecimal大数据类。
引入:
浮点型运算的时候直接+ * / 可能会出现数据失真(精度问题)。
BigDecimal可以解决浮点型运算数据失真的问题。
1
| System.out.println(0.1+0.2); //0.300000000000004
|
BigDicimal类:
包:java.math.
创建对象的方式(最好的方式:)
public static BigDecimal valueOf(double val) : 包装浮点数成为大数据对象。
方法声明
public BigDecimal add(BigDecimal value) 加法运算
public BigDecimal subtract(BigDecimal value) 减法运算
public BigDecimal multiply(BigDecimal value) 乘法运算
public BigDecimal divide(BigDecimal value) 除法运算
public double doubleValue() 把BigDecimal转换成double类型。
注意:BigDecimal有专门的方法比较大小和保留小数等。
1
2
3
4
5
6
7
8
9
10
11
| double a = 0.1;
double b = 0.2;
// 1.把浮点数转换成大数据对象运算
BigDecimal a1 = BigDecimal.valueOf(a);
BigDecimal b1 = BigDecimal.valueOf(b);
BigDecimal c1 = a1.add(b1); // 加法
BigDecimal c1 = a1.divide(b1); // 除法
// 结果可能需要继续使用!!!
// BigDecimal只是解决精度问题的手段,double数据才是我们的目的!!
double rs = c1.doubleValue();
|
包装类#
目标:包装类概念
引入:
Java认为一切皆对象。引用数据类型就是对象了。
但是在Java中8基本数据类型不是对象,只是表示一种数据的类型形式,这8种数据类型显得很突兀。
Java为了一切皆对象的思想统一,把8种基本数据类型转换成对应的类,这个类称为基本数据类型的包装类。
基本数据类型 包装类(引用数据类型)
byte Byte
short Short
int Integer(特殊)
long Long
float Float
double Double
char Character(特殊)
boolean Boolean
自动装箱:可以直接把基本数据类型的值或者变量赋值给包装类。
自动拆箱:可以把包装类的变量直接赋值给基本数据类型。
小结:
自动装箱:可以直接把基本数据类型的值或者变量赋值给包装类。
自动拆箱:可以把包装类的变量直接赋值给基本数据类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| int a = 12;
Integer a1 = 12; // 自动装箱
Integer a2 = a; // 自动装箱
Integer c = 100;
int c1 = c; // 自动拆箱
int d = 12;
Integer d1 = null; // 引用数据类型默认值为null
Integer d2 = 0;
Integer it = Integer.valueOf(12); // 手工装箱!
Integer it3 = 111;
int it33 = it3.intValue(); // 手工拆箱
|
**目标:**包装类的特殊功能
包装类作为类首相拥有了Object类的方法
包装类作为引用类型的变量可以存储null值
具体来看的特殊功能有:
1. 基本数据类型 -> 字符串类型。(没啥用)
- Xxx.toString(基本数据类型变量)方法。
- 字符串类型的数值 -> 对应的基本数据类型(有用)
- Xxx.parseXxx(字符串类型变量);
- Xxx.valueOf(字符串类型变量); 推荐使用
1
2
3
4
5
6
7
8
| //1.字符串类型变量 -> 字符串类型
Integer it = 100;
String itStr = Integer.toString(it);
//2.字符串类型 -> 对应值的基本数据类型
String numStr = "23";
int numInt = Integer.parseInt(numStr);//23
int numInt2 = Integer.valueOf(numStr);//23 推荐
|
StringAPI#
1.字符串类型转换成对应的类型
1
2
3
4
5
| String str = "123";
Integer num = Integer.valueOf(str);
String str2 = "12.5";
Double num2 = Double.valueOf(str2);
|
2.字符串内容比较
1
2
3
4
5
| String str = "i love you";
String str2 = "i love you";
boolean flag = str.equals(str2);
System.out.println(flag); //true
//注意: == 是用于比较基本数据类型中的值,字符串内容比较是用equals。
|
3.忽略大小写比较字符串内容
1
2
3
4
| String str = "i love you";
String str2 = "I LOVE YOU";
boolean flag = str.equalsIgnoreCase(str2);
System.out.println(flag); //true
|
4.返回字符串的长度
1
2
3
| String str = "i love you";
int length = str.length();
System.out.println(length); //10
|
5.根据索引返回某个位置的元素
1
2
3
| String str = "i love you";
char c = str.charAt(0);
System.out.println(c); //i
|
6.字符串转换为字符数组
1
2
3
4
5
| String str = "i love you";
char[] chars = str.toCharArray();
for (char c : chars) {
System.out.print(c); //i love you
}
|
7.截取字符串内容(包前不包后)
1
2
3
4
5
6
7
| String str = "i love you";
String substring = str.substring(0, 9);
System.out.println(str);//i love you
System.out.println(substring);//i love yo
String substring2 = str.substring(7); //从索引7开始截取到最后
System.out.println(substring2);//you
|
8.字符串内容替换
1
2
3
4
| String str = "i love you";
String str2 = str.replace("love", "**");
System.out.println(str);//i love you
System.out.println(str2);//i ** you
|
9.判断字符串是否包含某个内容
1
2
3
| String str = "i love you";
boolean flag = str.contains("you");
System.out.println(flag);//true
|
10.判断字符串是否以某个内容开始
1
2
3
| String str = "i love you";
boolean flag = str.startsWith("i");
System.out.println(flag);//true
|
11.按照某个内容将字符串分割成字符串数组返回
1
2
3
4
5
| String str = "i love you";
String[] str2 = str.split(" "); //["i","love","you"]
for (String s : str2) {
System.out.print(s); //iloveyou
}
|
12.返回一个删除前后空格的字符串
1
2
3
| String str = " i love you ";
String trim = str.trim();
System.out.println(trim);//i love you
|
13.返回指定字符第一次出现的字符串内的索引
1
2
3
| String str = "i love you";
int index = str.indexOf('y');
System.out.println(index);//7
|
14.返回指定字符的最后一次出现的字符串中的索引
1
2
3
| String str = "i love you";
int index = str.lastIndexOf('o');
System.out.println(index);//8
|
正则表达式#
目标:深入学习正则表达式的写法。
字符类
[abc] a、b 或 c(简单类)
[ ^abc] 任何字符,除了 a、b 或 c(否定)
[a-zA-Z] a 到 z 或 A 到 Z,两头的字母包括在内(范围)
[a-d[m-p]] a 到 d 或 m 到 p:[a-dm-p](并集)
[a-z&&[def23]] d、e 或 f(交集)
[a-z&&[ ^bc]] a 到 z,除了 b 和 c:[ad-z](减去)
[a-z&&[ ^m-p]] a 到 z,而非 m 到 p:[a-lq-z](减去)
预定义字符类
. 任何字符
\d 数字:[0-9]
\D 非数字: [ ^0-9]
\s 空白字符:[ \t\n\x0B\f\r]
\S 非空白字符:[ ^\s]
\w 单词字符:[a-zA-Z_0-9]
\W 非单词字符:[ ^\w]
以上正则匹配只能校验单个字符。
Greedy数量词
X? X,一次或一次也没有
X* X,零次或多次
X+ X,一次或多次
X{n} X,恰好 n 次
X{n,} X,至少 n 次
X{n,m} X,至少 n 次,但是不超过 m 次
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 boolean matches(String regex):判断是否与正则表达式匹配,匹配返回true
// 只能是 a b c
System.out.println("a".matches("[abc]")); // true
System.out.println("z".matches("[abc]")); // false
// 不能出现a b c
System.out.println("a".matches("[^abc]")); // false
System.out.println("z".matches("[^abc]")); // true
System.out.println("a".matches("\\d")); // false
System.out.println("3".matches("\\d")); // true
System.out.println("333".matches("\\d")); // false
System.out.println("z".matches("\\w")); // true
System.out.println("2".matches("\\w")); // true
System.out.println("21".matches("\\w")); // false
System.out.println("你".matches("\\w")); // false
// 以上正则匹配只能校验单个字符。
// 校验密码
// 必须是数字 字母 下划线 至少 6位
System.out.println("ssds3c".matches("\\w{6,}")); // true
System.out.println("ssdsc".matches("\\w{6,}")); // false
System.out.println("ssdsda232s你c".matches("\\w{6,}")); // false
// 验证。必须是数字和字符 必须是4位
System.out.println("dsd2".matches("[a-zA-Z0-9]{4}")); // true
System.out.println("A3dy".matches("[a-zA-Z0-9]{4}")); // true
System.out.println("A3dy2".matches("[a-zA-Z0-9]{4}")); // false
|
目标:正则表达式在方法中的应用。
public String[] split(String regex):
– 按照正则表达式匹配的内容进行分割字符串,反回一个字符串数组。
public String replaceAll(String regex,String newStr)
– 按照正则表达式匹配的内容进行替换
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
| // 1.split的基础用法
String names = "贾乃亮,王宝强,陈羽凡";
// 以“,”分割成字符串数组
String[] nameArrs = names.split(",");
for(int i = 0 ; i < nameArrs.length ; i++ ){
String name = nameArrs[i];
System.out.println(name);
}
System.out.println("----------------------");
// 2.split集合正则表达式做分割
String names1 = "贾乃亮lv434fda324王宝强87632fad2342423陈羽凡";
// 以匹配正则表达式的内容为分割点分割成字符串数组
String[] nameArrs1 = names1.split("\\w+");
for(int i = 0 ; i < nameArrs1.length ; i++ ){
String name = nameArrs1[i];
System.out.println(name);
}
System.out.println("----------------------");
// 3. public String replaceAll(String regex,String newStr)
String names2 = "贾乃亮lv434fda324王宝强87632fad2342423陈羽凡";
// 使用正则表达式定位出内容,替换成/
System.out.println(names2.replaceAll("\\w+" , "/"));
String names3 = "贾乃亮,王宝强,羽凡";
System.out.println(names3.replaceAll(",","-"));
|
目标:泛型的概述。
什么是泛型?
泛型就是一个标签:<数据类型>
泛型可以在编译阶段约束只能操作某种数据类型。
注意:JDK 1.7开始之后,泛型后面的申明可以省略不写!!
泛型和集合都只能支持引用数据类型,不支持基本数据类型。
泛型的好处:
泛型在编译阶段约束了操作的数据类型,从而不会出现类型转换异常。
体现的是Java的严谨性和规范性,数据类型,经常需要进行统一!
小结:
泛型可以在编译阶段约束只能操作某种数据类型。
泛型和集合都只能支持引用数据类型,不支持基本数据类型。
JDK 1.7开始之后,泛型后面的申明可以省略不写!!
1
2
3
4
| //ArrayList<String> lists = new ArrayList<String>();
ArrayList<String> lists = new ArrayList<>();
ArrayList<String> lists = new ArrayList();
// JDK 1.7开始之后,泛型后面的申明可以省略不写!!
|
目标:自定义泛型类
引入:
我们之前用的泛型都是别人写好的,接下来我们来自定义泛型类使用。
泛型类的概念:
使用了泛型定义的类就是泛型类。
泛型类的格式:
修饰符 class 类名<泛型变量>{
}
泛型变量建议使用 E , T , K , V
需求:模拟ArrayList集合自定义一个集合MyArrayList集合。
泛型类的核心思想:是把出现泛型变量的地方全部替换成传输的真实数据类型。
小结:
自定义泛型的核心思想:是把出现泛型变量的地方全部替换成传输的真实数据类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| //模拟ArrayList集合自定义一个集合MyArrayList集合。
public class GenericDemo {
public static void main(String[] args) {
MyArrayList<String> lists1 = new MyArrayList<>();
lists1.add("java");
lists1.add("mysql");
lists1.remove("java");
System.out.println(lists1);
}
}
class MyArrayList<E>{
private ArrayList lists = new ArrayList();
public void add(E e){
lists.add(e);
}
public void remove(E e){
lists.remove(e);
}
@Override
public String toString() {
return lists.toString();
}
}
|
目标:自定义泛型方法。
什么是泛型方法?
定义了泛型的方法就是泛型方法。
泛型方法的定义格式:
修饰符 <泛型变量> 返回值类型 方法名称(形参列表){
}
注意:方法定义了是什么泛型变量,后面就只能用什么泛型变量。
泛型类的核心思想:是把出现泛型变量的地方全部替换成传输的真实数据类型。
需求:给你任何一个类型的数组,都能返回它的内容。
小结:
泛型方法和泛型类可以做通用技术架构。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| public static void main(String[] args) {
Integer[] nums = {10 , 20 , 30 , 40 , 50};
String rs1 = arrToString(nums);
System.out.println(rs1);
String[] names = {"贾乃亮","王宝绿","陈羽凡"};
String rs2 = arrToString(names);
System.out.println(rs2);
}
public static <T> String arrToString(T[] nums){
StringBuilder sb = new StringBuilder();
sb.append("[");
if(nums!=null && nums.length > 0){
for(int i = 0 ; i < nums.length ; i++ ){
T ele = nums[i];
sb.append(i == nums.length-1 ? ele : ele+", ");
}
}
sb.append("]");
return sb.toString();
}
|
目标:泛型接口。
什么是泛型接口?
使用了泛型定义的接口就是泛型接口。
泛型接口的格式:
修饰符 interface 接口名称<泛型变量>{
}
小结:
泛型接口的核心思想,在实现接口的时候传入真实的数据类型
这样重写的方法就是对该数据类型进行操作!
1
2
3
4
5
6
7
| // 泛型接口
public interface Data<E> {
void add(E stu);
void delete(E stu);
void update(E stu);
E query(int id);
}
|
目标:泛型通配符。
注意:
虽然BMW和BENZ都继承了Car
但是ArrayList和ArrayList与ArrayList没有关系的!泛型没有继承关系!
通配符:?
?可以用在使用泛型的时候代表一切类型。
E , T , K , V是在定义泛型的时候使用代表一切类型。
泛型的上下限:
? extends Car : 那么?必须是Car或者其子类。(泛型的上限)
? super Car :那么?必须是Car或者其父类。(泛型的下限。不是很常见)
小结:
通配符:?可以用在使用泛型的时候代表一切类型。
? extends Car :那么?必须是Car或者其子类。(泛型的上限)
1
2
3
4
| // 定义一个方法,可以让很多汽车一起进入参加比赛
public static void run(ArrayList<? extends Car> cars){
}
|
Collection、Map#
Collection集合#
目标:Collection集合概述。
什么是集合?
集合是一个大小可变的容器。
容器中的每个数据称为一个元素。数据==元素。
集合的特点是:类型可以不确定,大小不固定。集合有很多种,不同的集合特点和使用场景不同。
数组:类型和长度一旦定义出来就都固定了。
集合和泛型都只能支持引用数据类型。
集合有啥用?
在开发中,很多时候元素的个数是不确定的。
而且经常要进行元素的增删改查操作,集合都是非常合适的。
开发中集合用的更多!!
Java中集合的代表是:Collection.
Collection集合是Java中集合的祖宗类。
学习Collection集合的功能,那么一切集合都可以用这些功能!!
Collection集合的体系:
Collection(接口)
/
Set(接口) List(接口)
/ \ /
HashSet(实现类) TreeSet<>(实现类) ArrayList(实现类) LinekdList<>(实现类)
/
LinkedHashSet<>(实现类)
集合的特点:
Set系列集合:添加的元素是无序,不重复,无索引的。
– HashSet:添加的元素是无序,不重复,无索引的。
– LinkedHashSet:添加的元素是有序,不重复,无索引的。
– TreeSet:不重复,无索引,按照大小默认升序排序!!
List系列集合:添加的元素是有序,可重复,有索引。
– ArrayList:添加的元素是有序,可重复,有索引。
– LinekdList:添加的元素是有序,可重复,有索引。
小结:
Collection是集合的祖宗类,Collection集合的功能是一切集合都可以直接使用的。
Collection集合API#
目标:Collection集合的常用API
Collection是集合的祖宗类,它的功能是全部集合都可以继承使用的,所以要学习它。
Collection API如下:
public boolean add(E e):把给定的对象添加到当前集合中 。
public void clear() :清空集合中所有的元素。
public boolean remove(E e): 把给定的对象在当前集合中删除。
public boolean contains(Object obj): 判断当前集合中是否包含给定的对象。
public boolean isEmpty(): 判断当前集合是否为空。
public int size(): 返回集合中元素的个数。
public Object[] toArray(): 把集合中的元素,存储到数组中
public boolean addAll(Collection<? extends E> c)将指定collection中的所有元素都添加到此collection中
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
| public static void main(String[] args) {
// HashSet:添加的元素是无序,不重复,无索引。
Collection<String> sets = new HashSet<>();
// 1.添加元素,添加成功返回true.
System.out.println(sets.add("贾乃亮")); // true
System.out.println(sets.add("贾乃亮")); // false
System.out.println(sets.add("王宝强")); // true
sets.add("陈羽凡");
System.out.println(sets); // 集合重写了toString()方法,默认打印出内容信息
// 2.清空集合的元素。
sets.clear();
// 3.判断集合是否为空 是空返回true 反之
System.out.println(sets.isEmpty()); // false
// 4.获取集合的大小
System.out.println(sets.size()); // 3
// 5.判断集合中是否包含某个元素 。
System.out.println(sets.contains("贾乃亮"));
// 6.删除某个元素:如果有多个重复元素默认删除前面的第一个!
sets.remove("陈羽凡");
System.out.println(sets);
// 7.把集合转换成数组
String[] arr = new String[sets.size()];
String[] arrs = sets.toArray(arr);
System.out.println("数组:"+ Arrays.toString(arrs));
String[] arrs1 = sets.toArray(String[]::new); // 以后再了解,指定转换的数组类型!JDK1.8
System.out.println("数组:"+ Arrays.toString(arrs1));
System.out.println("---------------------拓展---------------------------");
Collection<String> c1 = new ArrayList<>();
c1.add("李小璐");
c1.add("马蓉");
Collection<String> c2 = new ArrayList<>();
c2.add("白百合");
c1.addAll(c2); // 把c2集合的元素全部倒入到c1
System.out.println(c1);
}
|
Collection集合遍历方式#
目标:Collection集合的遍历方式。
什么是遍历? 为什么开发中要遍历?
遍历就是一个一个的把容器中的元素访问一遍。
开发中经常要统计元素的总和,找最值,找出某个数据然后干掉等等业务都需要遍历。
Collection集合的遍历方式是全部集合都可以直接使用的,所以我们学习它。
Collection集合的遍历方式有三种:
(1)迭代器。
(2)foreach(增强for循环)。
(3)JDK 1.8开始之后的新技术Lambda表达式(了解)
(4) 注意:Collection集合没有索引,不能用普通for循环。但是ArrayList有索引,可以用普通for循环
a.迭代器遍历集合。
– 方法:
public Iterator iterator(): 获取集合对应的迭代器,用来遍历集合中的元素的
E next():获取下一个元素值!
boolean hasNext():判断是否有下一个元素,有返回true ,反之。
– 流程:
1.先获取当前集合的迭代器
Iterator it = lists.iterator();
2.定义一个while循环,问一次取一次。
通过it.hasNext()询问是否有下一个元素,有就通过it.next()取出下一个元素。
1
2
3
4
5
6
7
| Collection<String> collection = new ArrayList<>();
String[] array = {"周志伟", "马宏伟", "张国伟"};
Collections.addAll(collection, array);
Iterator<String> iterator = collection.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
|
b.foreach(增强for循环)遍历集合。
foreach是一种遍历形式,可以遍历集合或者数组。
foreach遍历集合实际上是迭代器遍历的简化写法。
foreach遍历的关键是记住格式:
for(被遍历集合或者数组中元素的类型 变量名称 : 被遍历集合或者数组){
}
1
2
3
4
5
6
7
8
9
10
11
12
13
| //遍历集合
Collection<String> collection = new ArrayList<>();
String[] array = {"周志伟", "马宏伟", "张国伟"};
Collections.addAll(collection, array);
for (String s : collection) {
System.out.println(s);
}
//遍历数组
int[] ages = new int[]{17 , 18 , 38 , 21};
for (int age : ages) {
System.out.println(age);
}
|
c.JDK 1.8开始之后的新技术Lambda表达式。(暂时了解)
1
2
3
4
5
6
| Collection<String> collection = new ArrayList<>();
String[] array = {"周志伟", "马宏伟", "张国伟"};
Collections.addAll(collection, array);
collection.forEach( s -> {
System.out.println(s);
});
|
List集合#
目标:ArrayList集合。
Collection集合的体系:
Collection(接口)
/
Set(接口) List(接口)
/ \ / \
HashSet(实现类) TreeSet(实现类) LinkedList(实现类) Vector(线程安全) ArrayList(实现类)
/
LinkedHashSet(实现类)
Collection集合体系的特点:
Set系列集合: 添加的元素,是无序,不重复,无索引的。
– HashSet:添加的元素,是无序,不重复,无索引的。
– LinkedHashSet:添加的元素,是有序,不重复,无索引的。
List系列集合:添加的元素,是有序,可重复,有索引的。
– LinkedList: 添加的元素,是有序,可重复,有索引的。
– ArrayList: 添加的元素,是有序,可重复,有索引的。
– Vector 是线程安全的,速度慢,工作中很少使用。
List集合继承了Collection集合的全部功能,同时因为List系列集合有索引,因为List集合多了索引,所以多了很多按照索引操作元素的功能:ArrayList实现
类集合底层基于数组存储数据的,查询快,增删慢!
- public void add(int index, E element): 将指定的元素,添加到该集合中的指定位置上。
- E get(int index):返回集合中指定位置的元素。
- public E remove(int index): 移除列表中指定位置的元素, 返回的是被移除的元素。
- E set(int index, E element):用指定元素替换集合中指定位置的元素,返回更新前的元素值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // 1.创建一个ArrayList集合对象:这是一行经典代码!
// List:有序,可重复,有索引的。
List<String> lists = new ArrayList<>();
lists.add("java1");
lists.add("java1");
lists.add("java2");
lists.add("java2");
// 2.在某个索引位置插入元素。
lists.add(2,"MySQL");
// 3.根据索引删除元素,返回被删除元素
System.out.println(lists.remove(2));
// 4.根据索引获取元素
System.out.println(lists.get(2)); // java2
// 5.修改索引位置处的元素
lists.set(3,"Mybatis");
|
拓展:List系列集合的遍历方式有:4种。
List系列集合多了索引,所以多了一种按照索引遍历集合的for循环。
List遍历方式:
(1)for循环。
(2)迭代器。
(3)foreach。
(4)JDK 1.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
| List<String> lists = new ArrayList<>();
lists.add("java1");
lists.add("java2");
lists.add("java3");
/** (1)for循环。 */
for(int i = 0 ; i < lists.size() ; i++ ) {
String ele = lists.get(i);
System.out.println(ele);
}
System.out.println("-----------------------");
/** (2)迭代器。 */
Iterator<String> it = lists.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
System.out.println("-----------------------");
/** (3)foreach。 */
for(String ele : lists){
System.out.println(ele);
}
System.out.println("-----------------------");
/** (4)JDK 1.8开始之后的Lambda表达式*/
lists.forEach(s -> {
System.out.println(s);
});
|
目标:LinkedList集合
LinkedList也是List的实现类:底层是基于链表的,增删比较快,查询慢!!
LinkedList是支持双链表,定位前后的元素是非常快的,增删首尾的元素也是最快的
所以LinkedList除了拥有List集合的全部功能还多了很多操作首尾元素的特殊功能:
- 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):将元素推入此列表所表示的堆栈。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // 1.用LinkedList做一个队列:先进先出,后进后出。
LinkedList<String> queue = new LinkedList<>();
// 入队
queue.addLast("1号");
queue.addLast("2号");
queue.addLast("3号");
queue.addLast("4号");
System.out.println(queue); // [1号, 2号, 3号, 4号]
// 出队
System.out.println(queue.removeFirst());
System.out.println(queue.removeFirst());
System.out.println(queue);
// 做一个栈
LinkedList<String> stack = new LinkedList<>();
// 压栈
stack.push("第1颗子弹");
stack.push("第2颗子弹");
stack.push("第3颗子弹");
stack.push("第4颗子弹");
System.out.println(stack); // [第4颗子弹, 第3颗子弹, 第2颗子弹, 第1颗子弹]
// 弹栈
System.out.println(stack.pop());
System.out.println(stack.pop());
System.out.println(stack);
|
Set集合#
目标:HashSet集合
研究两个问题(面试热点):
1)Set集合添加的元素是不重复的,是如何去重复的?
2)Set集合元素无序的原因是什么?
解:1)
1.对于有值特性的,Set集合可以直接判断数值进行去重复。
2.对于引用数据类型的类对象,Set集合是按照如下流程进行是否重复的判断。
Set集合会让两两对象,先调用自己的hashCode()方法得到彼此的哈希值(所谓的内存地址)
然后比较两个对象的哈希值是否相同,如果不相同则直接认为两个对象不重复。
如果哈希值相同,会继续让两个对象进行equals比较内容是否相同,如果相同认为真的重复了
如果不相同认为不重复。
Set集合会先让对象调用hashCode()方法获取两个对象的哈希值比较
/
false true
/
不重复 继续让两个对象进行equals比较
/
false true
/
不重复 重复了
需求:只要对象内容一样,就希望集合认为它们重复了。重写hashCode和equals方法。
小结:
如果希望Set集合认为两个对象只要内容一样就重复了,必须重写对象的hashCode和equals方法。
解:2)
Set系列集合添加元素无序的根本原因是因为底层采用了哈希表存储元素。
JDK 1.8之前:哈希表 = 数组 + 链表 + (哈希算法)
JDK 1.8之后:哈希表 = 数组 + 链表 + 红黑树 + (哈希算法)
当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
1
2
3
4
5
6
7
8
9
10
| Student xiaoMing = new Student("小明",21);
Student xiaoHong = new Student("小明",21);
//未重写hashCode和equals方法时 不同对象的哈希值不同
System.out.println(xiaoMing.hashCode());//1784662007
System.out.println(xiaoHong.hashCode());//997110508
//需求:只要对象内容一样,就希望集合认为它们重复了。
// 重写hashCode和equals方法。
System.out.println(xiaoMing.hashCode());//23458775
System.out.println(xiaoHong.hashCode());//23458775
|
目标:LinkedHashSet
是HashSet的子类,元素是“有序” 不重复,无索引。
LinkedHashSet底层依然是使用哈希表存储元素的,
但是每个元素都额外带一个链来维护添加顺序!!
不光增删查快,还有序。缺点是多了一个存储顺序的链会占内存空间!!而且不允许重复,无索引。
总结:
- 如果希望元素可以重复,又有索引,查询要快用ArrayList集合。(用的最多)
- 如果希望元素可以重复,又有索引,增删要快要用LinkedList集合。(适合查询元素比较少的情况,经常要首尾操作元素的情况)
- 如果希望增删改查都很快,但是元素不重复以及无序无索引,那么用HashSet集合。
- 如果希望增删改查都很快且有序,但是元素不重复以及无索引,那么用LinkedHashSet集合。
目标:TreeSet集合。
TreeSet: 不重复,无索引,按照大小默认升序排序!!
TreeSet集合称为排序不重复集合,可以对元素进行默认的升序排序。
TreeSet集合自自排序的方式:
1.有值特性的元素直接可以升序排序。(浮点型,整型)
2.字符串类型的元素会按照首字符的编号排序。
3.对于自定义的引用数据类型,TreeSet默认无法排序,执行的时候直接报错,因为人家不知道排序规则。
自定义的引用数据类型的排序实现:
我们需要定制排序的大小规则,程序员定义大小规则的方案有2种:
a.直接为对象的类实现比较器规则接口Comparable,重写比较方法(拓展方式)
// 升序
// 如果程序员认为比较者大于被比较者 返回正数!
// 如果程序员认为比较者小于被比较者 返回负数!
// 如果程序员认为比较者等于被比较者 返回0!
b.直接为集合设置比较器Comparator对象,重写比较方法
// 升序
// 如果程序员认为比较者大于被比较者 返回正数!
// 如果程序员认为比较者小于被比较者 返回负数!
// 如果程序员认为比较者等于被比较者 返回0!
注意:如果类和集合都带有比较规则,优先使用集合自带的比较规则。
小结:
TreeSet集合对自定义引用数据类型排序,默认无法进行。
但是有两种方式可以让程序员定义大小规则:
a.直接为对象的类实现比较器规则接口Comparable,重写比较方法(拓展方式)
b.直接为集合设置比较器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
57
58
59
60
61
62
63
64
| // TreeSet : 排序不重复集合。
Set<Double> scores = new TreeSet<>();
scores.add(100.0);
scores.add(0.1);
scores.add(99.9);
System.out.println(scores);
// 字符串按照首字符的编号进行排序。
Set<String> names = new TreeSet<>();
names.add("Jack");
names.add("Dlei");
names.add("about");
names.add("曹雪芹");
names.add("bozai");
System.out.println(names);
// 引用数据类型定义TreeSet集合。
Set<Employee> employees = new TreeSet<>();
employees.add(new Employee("播仔",6500.0,21));
employees.add(new Employee("播妞",7500.0,19));
employees.add(new Employee("乔治",4500.0,23));
System.out.println(employees);
// public TreeSet(Comparator<? super E> comparator)
// 集合自带比较器对象
// 如果类和集合都存在大小规则,默认使用集合自带的规则进行大小排序!!
Set<Employee> employees1 = new TreeSet<>(new Comparator<Employee>() {
@Override
public int compare(Employee o1, Employee o2) {
// o1比较者 o2被比较者
// 升序
// 如果程序员认为比较者大于被比较者 返回正数!
// 如果程序员认为比较者小于被比较者 返回负数!
// 如果程序员认为比较者等于被比较者 返回0!
return o1.getAge() - o2.getAge();
}
});
employees1.add(new Employee("播仔",6500.0,21));
employees1.add(new Employee("播妞",7500.0,19));
employees1.add(new Employee("乔治",4500.0,23));
System.out.println(employees1);
public class Employee implements Comparable<Employee>{
// 重写了比较方法。
// e1.compareTo(o)
// 比较者:this
// 被比较者:o
// 需求:按照年龄比较
@Override
public int compareTo(Employee o) {
// 规则:Java规则 升序
// 如果程序员认为比较者大于被比较者 返回正数!
// 如果程序员认为比较者小于被比较者 返回负数!
// 如果程序员认为比较者等于被比较者 返回0!
// if(this.age > o.age){
// return 1;
// }else if(this.age < o.age){
// return -1;
// }
// return 0;
return this.age - o.age;
}
}
|
Collections工具类#
目标:Collections工具类的使用。
java.utils.Collections:是集合工具类
Collections并不属于集合,是用来操作集合的工具类。
Collections有几个常用的API:
- public static boolean addAll(Collection<? super T> c, T… elements)给集合对象批量添加元素!
- public static void shuffle(List list) :打乱集合顺序。
- public static void sort(List list):将集合中元素按照默认规则排序。
- public static void sort(List 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
22
23
24
25
26
27
| // 1.给集合批量添加元素
Collection<String> names = new ArrayList<>();
/**
* 参数一:被添加元素的集合
* 参数二:可变参数,一批元素
*/
Collections.addAll(names,"曹操","贾乃亮","王宝强","陈羽凡");
System.out.println(names);
//将数组的内容装入集合中
Integer[] array = {1, 2, 3};
HashSet<Integer> hashSet = new HashSet<>();
Collections.addAll(hashSet, array);
//hashSet.addAll(Arrays.asList(array)); 这也可以,不过参数要求是collection的子类,要将数组先转为集合
// 2.打乱集合的顺序:public static void shuffle(List<?> list)
// 注意:只能打乱有序的List集合。
List<String> newnames = new ArrayList<>();
Collections.addAll(newnames,"曹操","贾乃亮","王宝强","陈羽凡");
Collections.shuffle(newnames); // 打乱顺序
System.out.println(newnames);
// 3.public static <T> void sort(List<T> list):给List集合升序排序。
List<Double> scores = new ArrayList<>();
Collections.addAll(scores, 98.5, 66.5 , 59.5 , 66.5 , 99.5 );
Collections.sort(scores); // 默认升序排序!
System.out.println(scores);
|
可变参数#
目标:可变参数。
可变参数用在形参中可以接收多个数据。
可变参数的格式:数据类型… 参数名称
可变参数的作用:
传输参数非常灵活,方便。
可以不传输参数。
可以传输一个参数。
可以传输多个参数。
可以传输一个数组。
可变参数在方法内部本质上就是一个数组。
可变参数的注意事项:
1.一个形参列表中可变参数只能有一个!!
2.可变参数必须放在形参列表的最后面!!
1
2
3
4
5
6
7
8
9
10
11
12
| public static void main(String[] args) {
sum(); // 可以不传输参数。
sum(10); // 可以传输一个参数。
sum(10,20,30); // 可以传输多个参数。
sum(new int[]{10,30,50,70,90}); // 可以传输一个数组。
}
public static void sum(int...nums){
// 可变参数在方法内部本质上就是一个数组。
System.out.println("元素个数:"+nums.length);
System.out.println("元素内容:"+ Arrays.toString(nums));
}
|
Map集合#
目标:Map集合概述。
Map集合的完整格式:{key1=value1 , key2=value2 , key3=value3 , …}
Map集合的体系:(类似与Set集合体系)
Map<K , V>(接口,Map集合的祖宗类)
/
TreeMap<K , V> HashMap<K , V>(实现类,经典的,用的最多)
LinkedHashMap<K, V>(实现类)
Map集合的特点:
1.Map集合的特点都是由键决定的。
2.Map集合的键是无序,不重复的,无索引的。Map集合后面重复的键对应的元素会覆盖前面的整个元素!
3.Map集合的值无要求。
4.Map集合的键值对都可以为null。
HashMap:元素按照键是无序,不重复,无索引,值不做要求。
LinkedHashMap:元素按照键是有序,不重复,无索引,值不做要求。
Map集合API#
目标:Map集合的常用API(重点中的重点)
- public V put(K key, V value): 把指定的键与指定的值添加到Map集合中。
- public V remove(Object key): 把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。
- public V get(Object key) 根据指定的键,在Map集合中获取对应的值。
- public Set keySet(): 获取Map集合中所有的键,存储到Set集合中。
- public Set<Map.Entry<K,V» entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)。
- public Collection values(): 获取Map集合全部值集合。
- public boolean containKey(Object key): 判断该集合中是否有此键。
- public boolean containsValue(Object value): 判断该集合是否有此值。
- public void putAll(Map<? extends K, ? extends V> m): 将m集合的数据装入调用者里面
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
| Map<String , Integer> maps = new HashMap<>();
// 1.添加元素: 无序,不重复,无索引。
maps.put("iphoneX",10);
maps.put("iphoneX",100);// Map集合后面重复的键对应的元素会覆盖前面重复的整个元素!
maps.put("huawei",1000);
maps.put("手表",10);
// 2.清空集合
maps.clear();
// 3.判断集合是否为空,为空返回true ,反之!
System.out.println(maps.isEmpty());
// 4.根据键获取对应值。
Integer value = maps.get("娃娃");
// 5.根据键删除整个元素。(删除键会返回键的值)
maps.remove("iphoneX");
// 6.判断是否包含某个键 ,包含返回true ,反之
System.out.println(maps.containsKey("手表")); // true
System.out.println(maps.containsKey(10)); // false
// 7.判断是否包含某个值。
System.out.println(maps.containsValue(1000)); // true
System.out.println(maps.containsValue("30")); // false 包含的是整数30不是字符串。
// 8.获取全部键的集合:public Set<K> keySet()
// Map集合的键是无序不重复的,所以返回的是一个Set集合。
Set<String> keys = maps.keySet();
for (String key : keys) {
System.out.println(key);
}
// 9.获取全部值的集合:Collection<V> values();
// Map集合的值是不做要求的,可能重复,所以值要用Collection集合接收!
Collection<Integer> values = maps.values();
for (Integer value : values) {
System.out.println(value);
}
// 10.集合的大小
System.out.println(maps.size());
// 11.合并其他Map集合。(拓展)
Map<String,Integer> maps2 = new HashMap<>();
maps2.put("xiaoMi" , 1);
maps2.put("🔨手机" , 10);
maps2.put("手表" , 10000);
maps.putAll(maps2); // 把Map集合maps2的数据全部倒入到maps集合中去
|
Map集合遍历方式#
目标:Map集合的遍历方式。
Map集合的遍历方式有:3种。
(1)“键找值”的方式遍历:先获取Map集合全部的键,再根据遍历键找值。
(2)“键值对”的方式遍历:难度较大。
(3)JDK 1.8开始之后的新技术:Lambda表达式。(暂时了解)
a.“键找值”的方式遍历Map集合。
1.先获取Map集合的全部键的Set集合。
2.遍历键的Set集合,然后通过键找值。
b.“键值对”的方式遍历:
1.把Map集合转换成一个Set集合:Set<Map.Entry<K, V» entrySet();
2.此时键值对元素的类型就确定了,类型是键值对实体类型:Map.Entry<K, V>
3.接下来就可以用foreach遍历这个Set集合,类型用Map.Entry<K, V>
c.JDK 1.8开始之后的新技术:Lambda表达式。(暂时了解)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // a.键找值方式遍历
// 获取当前Map集合的全部键的集合 。
Set<String> keys = maps.keySet();
for (String key : keys) {
// 过键取对应的值
Integer value = maps.get(key);
}
// b.键值对的方式遍历
Set<Map.Entry<String,Integer>> entries = maps.entrySet();
for (Map.Entry<String, Integer> entry : entries) {
String key = entry.getKey();
Integer value = entry.getValue();
}
// c.Lambda表达式
maps.forEach((k , v) -> {
System.out.println(k+"==>"+v);
});
|
LinkedHashMap#
目标:LinkedHashMap介绍。
添加的元素按照键有序,不重复的。
HashSet集合相当于是HashMap集合的键都不带值。
LinkedHashSet集合相当于是LinkedHashMap集合的键都不带值。
底层原理完全一样,都是基于哈希表按照键存储数据的,
只是HashMap或者LinkedHashMap的键都多一个附属值。
小结:
HashMap集合是无序不重复的键值对集合。
LinkedHashMap集合是有序不重复的键值对集合。
他们都是基于哈希表存储数据,增删改查都很好。
TreeMap#
目标:TreeMap集合的特点和使用。
TreeMap集合按照键是可排序不重复的键值对集合。(默认升序)
TreeMap集合按照键排序的特点与TreeSet是完全一样的。
小结:
TreeMap集合和TreeSet集合都是排序不重复集合
TreeSet集合的底层是基于TreeMap,只是键没有附属值而已。
所以TreeMap集合指定大小规则有2种方式:
a.直接为对象的类实现比较器规则接口Comparable,重写比较方法(拓展方式)
b.直接为集合设置比较器Comparator对象,重写比较方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| //a.
public class Pig implements Comparable{
// 比较者: this
// 被比较者: o
// 需求:按照价格排序!
@Override
public int compareTo(Object o) {
// 浮点型的大小比较建议使用Java自己的API:
// public static int compare(double d1, double d2)
return Double.compare(this.price , ((Pig)o).price);
}
}
//b.
//public TreeMap(Comparator<? super K> comparator)
Map<Pig,String> pigs1 = new TreeMap<>(new Comparator<Pig>() {
@Override
public int compare(Pig p1, Pig p2) {
return Double.compare(p1.getWeight() , p2.getWeight());
}
});
|
异常分类#
目标:异常的概念和体系。
什么是异常?
异常是程序在"编译"或者"执行"的过程中可能出现的问题。
异常是应该尽量提前避免的。
异常可能也是无法做到绝对避免的,异常可能有太多情况了,开发中只能提前干预!!
异常一旦出现了,如果没有提前处理,程序就会退出JVM虚拟机而终止,开发中异常是需要提前处理的。
研究异常并且避免异常,然后提前处理异常,体现的是程序的安全, 健壮性!!!
Java会为常见的代码异常都设计一个类来代表。
异常的体系:
Java中异常继承的根类是:Throwable。
Throwable(根类,不是异常类)
/
Error Exception(异常,需要研究和处理)
/
编译时异常 RuntimeException(运行时异常)
Error : 错误的意思,严重错误Error,无法通过处理的错误,一旦出现,程序员无能为力了,
只能重启系统,优化项目。
比如内存奔溃,JVM本身的奔溃。这个程序员无需理会。
Exception: 才是异常类,它才是开发中代码在编译或者执行的过程中可能出现的错误,
它是需要提前处理的。以便程序更健壮!
Exception异常的分类:
1.编译时异常:继承自Exception的异常或者其子类,编译阶段就会报错,必须程序员处理的。否则代码编译就不能通过!!
2.运行时异常:继承自RuntimeException的异常或者其子类,编译阶段是不会出错的,它是在运行时阶段可能出现,运行时异常可以处理也可以不处理,
编译阶段是不会出错的,但是运行阶段可能出现,还是建议提前处理!!
小结:
异常是程序在编译或者运行的过程中可能出现的错误!!
异常分为2类:编译时异常,运行时异常。
– 编译时异常:继承了Exception,编译阶段就报错,必须处理,否则代码不通过。
– 运行时异常:继承了RuntimeException,编译阶段不会报错,运行时才可能出现。
异常一旦真的出现,程序会终止,所以要研究异常,避免异常,处理异常,程序更健壮!!
运行时异常#
运行时异常的概念:
继承自RuntimeException的异常或者其子类,
编译阶段是不会出错的,它是在运行时阶段可能出现的错误,
运行时异常编译阶段可以处理也可以不处理,代码编译都能通过!!
1.数组索引越界异常: ArrayIndexOutOfBoundsException。
2.空指针异常 : NullPointerException。直接输出没有问题。但是调用空指针的变量的功能就会报错!!
3.类型转换异常:ClassCastException。
4.迭代器遍历没有此元素异常:NoSuchElementException。
5.数学操作异常:ArithmeticException。
6.数字转换异常: NumberFormatException。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| /** 1.数组索引越界异常: ArrayIndexOutOfBoundsException。*/
int[] arrs = {10 ,20 ,30};
System.out.println(arrs[2]);
// System.out.println(arrs[3]); // 此处出现了数组索引越界异常。代码在此处直接执行死亡!
/** 2.空指针异常 : NullPointerException。直接输出没有问题。但是调用空指针的变量的功能就会报错!! */
String name = null ;
System.out.println(name); // 直接输出没有问题
// System.out.println(name.length()); // 此处出现了空指针异常。代码在此处直接执行死亡!
/** 3.类型转换异常:ClassCastException。 */
Object o = "齐天大圣";
//Integer s = (Integer) o; // 此处出现了类型转换异常。代码在此处直接执行死亡!
/** 5.数学操作异常:ArithmeticException。 */
int c = 10 / 0 ; // 此处出现了数学操作异常。代码在此处直接执行死亡!
/** 6.数字转换异常: NumberFormatException。 */
String num = "23aa";
Integer it = Integer.valueOf(num); // 此处出现了数字转换异常。代码在此处直接执行死亡!
|
编译时异常#
编译时异常:继承自Exception的异常或者其子类,没有继承RuntimeException"编译时异常是编译阶段就会报错”,必须程序
员编译阶段就处理的。否则代码编译就报错!!
编译时异常的作用是什么:
- 是担心程序员的技术不行,在编译阶段就爆出一个错误, 目的在于提醒!
- 提醒程序员这里很可能出错,请检查并注意不要出bug。
- 编译时异常是可遇不可求。遇到了就遇到了呗。
异常处理过程#
(1)默认会在出现异常的代码那里自动的创建一个异常对象:ArithmeticException。
(2)异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给JVM虚拟机。
(3)虚拟机接收到异常对象后,先在控制台直接输出异常栈信息数据。
(4)直接从当前执行的异常点干掉当前程序。
(5)后续代码没有机会执行了,因为程序已经死亡。
编译时异常的3种处理方式#
抛出异常格式:
方法 throws 异常1 , 异常2 , ..{
}
建议抛出异常的方式:代表可以抛出一切异常,
方法 throws Exception{
}
方式一:
在出现编译时异常的地方层层把异常抛出去给调用者,调用者最终抛出给JVM虚拟机。
JVM虚拟机输出异常信息,直接干掉程序,这种方式与默认方式是一样的。
虽然可以解决代码编译时的错误,但是一旦运行时真的出现异常,程序还是会立即死亡!
方式二:在出现异常的地方自己处理,谁出现谁处理。
自己捕获异常和处理异常的格式:捕获处理
try{
// 监视可能出现异常的代码!
}catch(异常类型1 变量){
// 处理异常
}catch(异常类型2 变量){
// 处理异常
}…
监视捕获处理异常企业级写法:
try{
// 可能出现异常的代码!
}catch (Exception e){
e.printStackTrace(); // 直接打印异常栈信息
}
Exception可以捕获处理一切异常类型!
但是从理论上来说,这种方式不是最好的,上层调用者不能直接知道底层的执行情况!
方式三: 在出现异常的地方把异常一层一层的抛出给最外层调用者,最外层调用者集中捕获处理!!(规范做法)
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) {
System.out.println("程序开始。。。。");
try {
parseDate("2013-03-23 10:19:23");
System.out.println("功能成功执行!!");
} catch (Exception e) {
e.printStackTrace();
System.out.println("功能执行失败!!");
}
System.out.println("程序结束。。。。。");
}
// 可以拦截所以异常!
public static void parseDate(String time) throws Exception {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date d = sdf.parse(time);
System.out.println(d);
InputStream is = new FileInputStream("D:/meinv.png");
}
|
运行时异常的处理方式#
运行时异常在编译阶段是不会报错,在运行阶段才会出错。
运行时异常在编译阶段不处理也不会报错,但是运行时如果出错了程序还是会死亡,所以运行时异常也建议要处理。
运行时异常是自动往外抛出的,不需要我们手工抛出。
运行时异常的处理规范:直接在最外层捕获处理即可,底层会自动抛出!!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public static void main(String[] args) {
System.out.println("程序开始。。。。");
try{
chu(10 , 0);
System.out.println("操作成功!");
}catch (Exception e){
e.printStackTrace();
System.out.println("操作失败!");
}
System.out.println("程序结束。。。。");
}
public static void chu(int a , int b) {
System.out.println( a / b );
}
|
目标:异常的语法注意(了解)
- 运行时异常被抛出可以不处理。可以自动抛出,编译时异常必须处理.按照规范都应该处理!
- 重写方法申明抛出的异常,应该与父类被重写方法申明抛出的异常一样或者范围更小
- 方法默认都可以自动抛出运行时异常! throws RuntimeException可以省略不写!!
- 当多异常处理时,捕获处理,前边的异常类不能是后边异常类的父类。
- 在try/catch后可以追加finally代码块,其中的代码一定会被执行,通常用于资源回收操作。
finally关键字#
目标:finally关键字
用在捕获处理的异常格式中的,放在最后面。
try{
// 可能出现异常的代码!
}catch(Exception e){
e.printStackTrace();
}finally{
// 无论代码是出现异常还是正常执行,最终一定要执行这里的代码!!
}
try: 1次。
catch:0-N次 (如果有finally那么catch可以没有!!)
finally: 0-1次
finally的作用: 可以在代码执行完毕以后进行资源的释放操作。
什么是资源:资源都是实现了Closeable接口的,都自带close()关闭方法!!
自定义异常#
目标: 自定义异常
自定义异常:
自定义编译时异常.
a.定义一个异常类继承Exception.
b.重写构造器。
c.在出现异常的地方用throw new 自定义对象抛出!
编译时异常是编译阶段就报错,提醒更加强烈,一定需要处理!!
自定义运行时异常.
a.定义一个异常类继承RuntimeException.
b.重写构造器。
c.在出现异常的地方用throw new 自定义对象抛出!
提醒不强烈,编译阶段不报错!!运行时才可能出现!!
小结:
自定义异常是程序员自己定义的异常
继承Exception/RuntimeException,重写构造器。
在出现异常的地方用throw new 自定义异常对象抛出!
throws:用在方法上,用于抛出方法中的异常。
throw:用在出现异常的地方,用于创建异常对象且立即从此处抛出!
多线程#
目标:多线程的概述。(并发编程)
什么是进程?
程序是静止的,运行中的程序就是进程。
进程的三个特征:
1.动态性 : 进程是运行中的程序,要动态的占用内存,CPU和网络等资源。
2.独立性 : 进程与进程之间是相互独立的,彼此有自己的独立内存区域。
3.并发性 : 假如CPU是单核,同一个时刻其实内存中只有一个进程在被执行。
CPU会分时轮询切换依次为每个进程服务,因为切换的速度非常快,给我们的感觉这些进程在同时执行,这就是并发性。
并行:同一个时刻同时有多个在执行。
什么是线程?
线程是属于进程的。一个进程可以包含多个线程,这就是多线程。
线程是进程中的一个独立执行单元。
线程创建开销相对于进程来说比较小。
线程也支持“并发性”。
线程的作用:
可以提高程序的效率,线程也支持并发性,可以有更多机会得到CPU。
多线程可以解决很多业务模型。
大型高并发技术的核心技术。
目标:线程的创建方式(3种)
多线程是很有用的,我们在进程中创建线程的方式有三种:
(1)直接定义一个类继承线程类Thread,重写run()方法,创建线程对象调用线程对象的start()方法启动线程。
(2)定义一个线程任务类实现Runnable接口,重写run()方法,创建线程任务对象,把线程任务对象包装成线程对象, 调用线程对象的start()方法启动线程。
(3)实现Callable接口(拓展)。
(4)从线程池获取
Thread的构造器:
– public Thread(){}
– public Thread(String name){}
– public Thread(Runnable target){}
– public Thread(Runnable target,String name){}
a.继承Thread类的方式
– 1.定义一个线程类继承Thread类。
– 2.重写run()方法
– 3.创建一个新的线程对象。
– 4.调用线程对象的start()方法启动线程。
b.实现Runnable接口的方式。
– 1.创建一个线程任务类实现Runnable接口。
– 2.重写run()方法
– 3.创建一个线程任务对象。
– 4.把线程任务对象包装成线程对象
– 5.调用线程对象的start()方法启动线程。
c.实现Callable接口。
– 1.定义一个线程任务类实现Callable接口,申明线程执行的结果类型。
– 2.重写线程任务类的call方法,这个方法可以直接返回执行的结果。
– 3.创建一个Callable的线程任务对象。
– 4.把Callable的线程任务对象包装成一个未来任务对象。
– 5.把未来任务对象包装成线程对象。
– 6.调用线程的start()方法启动线程
–7.从未来任务对象获取线程的返回结果。
三种方式分析:
- 方式a是继承Thread类,无法再继承其他类,扩展性差。
- 方式b是实现Runnable接口,扩展性好,run()没有返回结果
- 方式c是实现Callable接口,扩展性好,run()有返回结果
小结:
线程类是继承了Thread的类。
启动线程必须调用start()方法。
多线程是并发抢占CPU执行,所以在执行的过程中会出现并发随机性。
线程创建的方式a:
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
| public class ThreadDemo {
// 启动后的ThreadDemo当成一个进程。
// main方法是由主线程执行的,理解成main方法就是一个主线程
public static void main(String[] args) {
// 3.创建一个线程对象
Thread t = new MyThread();
// 4.调用线程对象的start()方法启动线程,最终还是执行run()方法!
t.start();
for(int i = 0 ; i < 100 ; i++ ){
System.out.println("main线程输出:"+i);
}
}
}
// 1.定义一个线程类继承Thread类。
class MyThread extends Thread{
// 2.重写run()方法
@Override
public void run() {
// 线程的执行方法。
for(int i = 0 ; i < 100 ; i++ ){
System.out.println("子线程输出:"+i);
}
}
}
|
线程创建的方式b:
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 ThreadDemo {
public static void main(String[] args) {
// 3.创建一个线程任务对象(注意:线程任务对象不是线程对象,只是执行线程的任务的)
Runnable target = new MyRunnable();
// 4.把线程任务对象包装成线程对象.且可以指定线程名称
// Thread t = new Thread(target);
Thread t = new Thread(target,"1号线程");
// 5.调用线程对象的start()方法启动线程
t.start();
Thread t2 = new Thread(target);
// 调用线程对象的start()方法启动线程
t2.start();
for(int i = 0 ; i < 10 ; i++ ){
System.out.println(Thread.currentThread().getName()+"==>"+i);
}
}
}
// 1.创建一个线程任务类实现Runnable接口。
class MyRunnable implements Runnable{
// 2.重写run()方法
@Override
public void run() {
for(int i = 0 ; i < 10 ; i++ ){
System.out.println(Thread.currentThread().getName()+"==>"+i);
}
}
}
|
线程创建的方式c:
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
| public class ThreadDemo {
public static void main(String[] args) {
// 3.创建一个Callable的线程任务对象
Callable call = new MyCallable();
// 4.把Callable任务对象包装成一个未来任务对象
// -- public FutureTask(Callable<V> callable)
// 未来任务对象是啥,有啥用?
// -- 未来任务对象其实就是一个Runnable对象:这样就可以被包装成线程对象!
// -- 未来任务对象可以在线程执行完毕之后去得到线程执行的结果。
FutureTask<String> task = new FutureTask<>(call);
// 5.把未来任务对象包装成线程对象
Thread t = new Thread(task);
// 6.启动线程对象
t.start();
for(int i = 1 ; i <= 10 ; i++ ){
System.out.println(Thread.currentThread().getName()+" => " + i);
}
// 在最后去获取线程执行的结果,如果线程没有结果,让出CPU等线程执行完再来取结果
try {
String rs = task.get(); // 获取call方法返回的结果(正常/异常结果)
System.out.println(rs);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 1.创建一个线程任务类实现Callable接口,申明线程返回的结果类型
class MyCallable implements Callable<String>{
// 2.重写线程任务类的call方法!
@Override
public String call() throws Exception {
// 需求:计算1-10的和返回
int sum = 0 ;
for(int i = 1 ; i <= 10 ; i++ ){
System.out.println(Thread.currentThread().getName()+" => " + i);
sum+=i;
}
return Thread.currentThread().getName()+"执行的结果是:"+sum;
}
}
|
目标:线程的注意事项。
1.线程的启动必须调用start()方法。否则当成普通类处理。
– 如果线程直接调用run()方法,相当于变成了普通类的执行,此时将只有主线程在执行他们!
– start()方法底层其实是给CPU注册当前线程,并且触发run()方法执行
2.建议线程先创建子线程,主线程的任务放在之后。否则主线程永远是先执行完!
线程的常见API#
目标:线程的常用API.
Thread类的API:
1.public void setName(String name):给当前线程取名字。
2.public void getName():获取当前线程的名字。
– 线程存在默认名称,子线程的默认名称是:Thread-索引。
– 主线程的默认名称就是:main
3.public static Thread currentThread()
– 获取当前线程对象,这个代码在哪个线程中,就得到哪个线程对象。
4.public static void sleep(long time):让当前线程休眠多少毫秒再继续执行。
线程同步#
目标:线程同步的三种方式
线程同步的作用:就是为了解决线程安全问题的方案。
**线程同步解决线程安全问题的核心思想:**让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
线程同步的做法:加锁,是把共享资源进行上锁,每次只能一个线程进入访问完毕以后,其他线程才能进来。
线程同步的方式有三种:
(1)同步代码块。
(2)同步方法。
(3)lock显示锁。
a.同步代码块
作用:把出现线程安全问题的核心代码给上锁,每次只能一个线程进入
执行完毕以后自动解锁,其他线程才可以进来执行。
格式:
synchronized(锁对象){
// 访问共享资源的核心代码
}
锁对象:理论上可以是任意的“唯一”对象即可。
原则上:锁对象建议使用共享资源。
– 在实例方法中建议用this作为锁对象。此时this正好是共享资源!必须代码高度面向对象
– 在静态方法中建议用类名.class字节码作为锁对象。
b.同步方法
作用: 把出现线程安全问题的核心方法给锁起来,
每次只能一个线程进入访问,其他线程必须在方法外面等待。
格式: public synchronized void drawMoney(double money) { // 访问共享资源的核心代码 }
用法: 直接给方法加上一个修饰符 synchronized.
原理: 同步方法的原理和同步代码块的底层原理其实是完全一样的,只是同步方法是把整个方法的代码都锁起来的。
同步方法其实底层也是有锁对象的:
如果方法是实例方法:同步方法默认用this作为的锁对象。
如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
c.lock显示锁
java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作
Lock锁也称同步锁,加锁与释放锁方法化了,如下:
public void lock() :加同步锁。
public void unlock():释放同步锁。
格式:private final Lock lock = new ReentrantLock(); //要求lock对象唯一,可以放在共享资源类里创建
public void drawMoney(double money) {
lock.lock(); // 上锁~!
// 访问共享资源的核心代码
lock.unlock(); // 解锁~!
}
线程通信#
目标:线程通信(了解原理,代码几乎不用)
线程通信:多个线程因为在同一个进程中,所以互相通信比较容易的。
线程通信的经典模型:生产者与消费者问题。
生产者负责生成商品,消费者负责消费商品。
生产不能过剩,消费不能没有。
注意:线程通信一定是多个线程在操作同一个资源才需要进行通信。
线程通信必须先保证线程安全,否则毫无意义,代码也会报错!
线程通信的核心方法:
public void wait(): 让当前线程进入到等待状态 此方法必须锁对象调用
public void notify() : 唤醒当前锁对象上等待状态的某个线程 此方法必须锁对象调用
public void notifyAll() : 唤醒当前锁对象上等待状态的全部线程 此方法必须锁对象调用
小结:
是一种等待唤醒机制。
必须是在同一个共享资源才需要通信,而且必须保证线程安全。
线程状态#
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在API中java.lang.Thread.State这个枚举中给出了六种线程状态:
| 线程状态 | 导致状态发生条件 |
|---|
| NEW(新建) | 线程刚被创建,但是并未启动。还没有调用start方法。MyThread t = new MyThread只有线程对象,没有线程特征 |
| Runnable(可运行) | 又可以分为2种,就绪态和运行态。就绪态就是调用的start方法,但是还没有竞争到CPU。运行态就是调用的start方法后竞争到了CPU。 |
| Blocked(锁阻塞) | 一个线程试图获取一个锁,但该锁被其他线程持有,则该线程进入Blocked状态。当该线程获取锁后,线程状态变成运行态。 |
| Waiting(无限等待) | 线程获取锁后,调用wait方法。进入这个状态后不能自动唤醒,必须等待另一个线程调用notify或notifyAll才能唤醒。唤醒后竞争锁。注意:进入等待状态后会释放锁 |
| TimedWaiting(计时等待) | 有两种方法能进入该状态。1.调用sleep(4000),等待时不会释放锁,时间结束后恢复运行态。2.调用wait(4000),等待时会释放锁,时间结束后竞争锁。 |
| Teminated(被终止) | 因为run方法正常退出死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
线程池#
目标:什么是线程池。
线程池: 其实就是一个容纳多个线程的容器,其中的线程可以反复的使用,
省去了频繁创建和销毁线程对象的操作,无需反复创建线程而消耗过多资源。
为什么要用线程池:
合理利用线程池能够带来三个好处
1.降低资源消耗。
– 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
2.提高响应速度
– 不需要频繁的创建线程,如果有线程可以直接用,不会出现系统僵死!
3.提高线程的可管理性(线程池可以约束系统最多只能有多少个线程,不会因为线程过多而死机)
线程池的核心思想:线程复用,同一个线程可以被重复使用,来处理多个任务。
目标:创建一个线程池。
线程池在Java中的代表类:ExecutorService(接口)。
Java在Executors类下提供了一个静态方法得到一个线程池的对象:
1.public static ExecutorService newFixedThreadPool(int nThreads):创建一个线程池返回。
ExecutorService提交线程任务对象执行的方法:
1.Future submit(Runnable task):提交一个Runnable的任务对象给线程池执行。
2.Future submit(Callable task):提交一个Callable的任务对象给线程池执行。
小结:
pools.shutdown(); // 等待任务执行完毕以后才会关闭线程池
pools.shutdownNow(); // 立即关闭线程池的代码,无论任务是否执行完毕!
线程池中的线程可以被复用,线程用完以后可以继续去执行其他任务。
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
| public class ThreadPoolsDemo02 {
public static void main(String[] args) {
// a.创建一个线程池,指定线程的固定数量是3.
ExecutorService pools = Executors.newFixedThreadPool(3);
// b.创建线程的任务对象。
Runnable target = new MyRunnable();
// c.把线程任务放入到线程池中去执行。
pools.submit(target); // 提交任务,此时会创建一个新线程,自动启动线程执行!
pools.submit(target); // 提交任务,此时会创建一个新线程,自动启动线程执行!
pools.submit(target); // 提交任务,此时会创建一个新线程,自动启动线程执行!
pools.submit(target); // 不会再创建新线程,会复用之前的线程来处理这个任务
pools.shutdown(); // 等待任务执行完毕以后才会关闭线程池
pools.shutdownNow(); // 立即关闭线程池的代码,无论任务是否执行完毕!
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for(int i = 0 ; i < 5 ; i++ ){
System.out.println(Thread.currentThread().getName()+" => "+i);
}
}
}
|
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
| public class ThreadPoolsDemo03 {
public static void main(String[] args) {
// a.创建一个线程池,指定线程的固定数量是3.
// new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
ExecutorService pools = Executors.newFixedThreadPool(3);
Future<String> t1 = pools.submit(new MyCallable(10)); // 提交任务,此时会创建一个新线程,自动启动线程执行!
Future<String> t2 = pools.submit(new MyCallable(20)); // 提交任务,此时会创建一个新线程,自动启动线程执行!
Future<String> t3 = pools.submit(new MyCallable(30)); // 提交任务,此时会创建一个新线程,自动启动线程执行!
Future<String> t4 = pools.submit(new MyCallable(40)); // 复用之前的某个线程
try{
// b.可以得到线程池执行的任务结果
String rs1 = t1.get(); //pool-1-thread-1计算1-10的和:55
String rs2 = t2.get(); //pool-1-thread-2计算1-20的和:210
String rs3 = t3.get(); //pool-1-thread-3计算1-30的和:465
String rs4 = t4.get(); //pool-1-thread-3计算1-40的和:820
}catch (Exception e){
e.printStackTrace();
}
}
}
// 1.定义一个线程任务类实现Callable接口 , 申明线程执行的结果类型。
class MyCallable implements Callable<String>{
private int n;
public MyCallable(int n){
this.n = n;
}
// 2.重写线程任务类的call方法,这个方法可以直接返回执行的结果。
@Override
public String call() throws Exception {
int sum = 0 ;
for(int i = 1 ; i <= n ; i++){
System.out.println(Thread.currentThread().getName()+" => "+i);
sum += i ;
}
return Thread.currentThread().getName()+"计算1-"+n+"的和:"+sum;
}
}
|
目标:死锁。
死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
java 死锁产生的四个必要条件:
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用。
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待循环队列:p1要p2的资源,p2要p1的资源。这样就形成了一个等待环路
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失
小结:
死锁是多个线程满足上述四个条件才会形成,死锁需要尽量避免。
死锁一般存在资源的嵌套请求!
产生死锁的情况:
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
| // 1.至少需要两个资源,每个资源只需要1份。
public static Object resources1 = new Object();
public static Object resources2 = new Object();
public static void main(String[] args) {
// 2.创建2个线程。
new Thread(new Runnable() {
@Override
public void run() {
// 线程1:占用资源1 ,请求资源2
synchronized (resources1){
System.out.println("线程1已经占用了资源1,开始请求资源2");
try {
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (resources2){
System.out.println("线程1已经占用了资源2");
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (resources2){
System.out.println("线程2已经占用了资源2,开始请求资源1");
try {
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (resources1){
System.out.println("线程2已经占用了资源1");
}
}
}
}).start();
}
|
volatile关键字#
目标:volatile关键字学习
引入:线程修改了某个成员变量的值,但是在主线程中读取到的还是之前的值,修改后的值无法读取到。
原因:按照JMM模型,所有的成员变量和静态变量都存在于主内存中,主内存中的变量可以被多个线程共享。
每个线程都存在一个专属于自己的工作内存,工作内存一开始存储的是成员变量的副本。
所以线程很多时候都是直接访问自己工作内存中的该变量,其他线程对主内存变量值的修改将不可见!!
JMM有以下规定:
- 所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题
- 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
- 线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
- 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。
本地内存和主内存的关系:

解决此问题:
希望所有线程对于主内存的成员变量修改,其他线程是可见的。
(1)加锁:可以实现其他线程对变量修改的可见性。
某一个线程进入synchronized代码块前后,执行过程入如下:
a.线程获得锁
b.清空工作内存
c.从主内存拷贝共享变量最新的值到工作内存成为副本
(2)可以给成员变量加上一个volatile关键字,立即就实现了成员变量多线程修改的可见性。
小结:
可以给成员变量加上一个volatile关键字,当一个线程修改了这个成员变量的值,其他线程可以立即看到修改后的值并使用!
volatile与synchronized的区别。
- volatile只能修饰
成员变量和静态成员变量,而synchronized可以修饰方法,代码块,对象,并不修饰变量。 volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制,能够保证线程安全。
原子性#
并发包ConcurrentHashMap#
并发包CountDownLatch#
并发包CyclicBarrier#
并发包Semaphore#
并发包Exchanger#
java8特性#
Lambda表达式#
目标:Lambda表达式的概述。
什么是Lambda表达式?
Lambda表达式是JDK1.8开始之后的新技术,是一种代码的新语法。
是一种特殊写法,
作用:“核心目的是为了简化匿名内部类的代码写法”。
Lambda表达式的格式:
(匿名内部类被重写方法的形参列表) -> {
被重写方法的方法体代码。
}
-> 就是一个新语法,没有实际含义,但是不能省略!
Lambda表达式的使用前提:
(1)Lambda表达式并不能简化所有匿名内部类的写法。
(2)Lambda表达式只能简化接口中只有一个抽象方法的匿名内部类形式。
Lambda表达式只能简化函数式接口的匿名内部类写法:
a.首先必须是接口。
b.接口中只能有一个抽象方法。
小结:
Lambda表达式只能简化接口中只有一个抽象方法的匿名内部类写法。
接口中只有一个抽象方法的接口称为函数式接口。
Lambda只能简化函数式接口的匿名内部类写法。
目标:Lambda表达式简化Runnable接口的匿名内部类写法
@FunctionalInterface函数式接口注解:
一旦某个接口加上了这个注解,这个接口只能有且仅有一个抽象方法。
这个接口就可以被Lambda表达式简化。
1
2
3
4
5
6
7
8
9
10
11
12
| //原来匿名内部类写法
Thread thread = new Thread(new Runnable(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行");
}
});
//Lambda表达式
Thread thread1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"执行");
});
|
Lambda表达式的省略写法(进一步在Lambda表达式的基础上继续简化)
(1)如果Lambda表达式的方法体代码只有一行代码。可以省略大括号不写,同时要省略分号!
(2)如果Lambda表达式的方法体代码只有一行代码。可以省略大括号不写。
此时,如果这行代码是return语句,必须省略return不写,同时也必须省略";“不写
(3)参数类型可以省略不写。
(4)如果只有一个参数,参数类型可以省略,同时()也可以省略。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| names.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
names.forEach((String s) -> {
System.out.println(s);
});
names.forEach((s) -> {
System.out.println(s);
});
names.forEach(s -> {
System.out.println(s);
});
names.forEach(s -> System.out.println(s));
//方法引用
names.forEach(System.out::println);
|
方法引用#
目标:方法引用的概述。(了解)
方法引用:
方法引用是为了进一步简化Lambda表达式的写法。
方法引用的格式:类型或者对象::引用的方法。
关键语法是:“::”
小结:
方法引用可以进一步简化Lambda表达式的写法。
关键语法是:“::”
目标:方法引用四种形式
1.静态方法的引用。
2.实例方法的引用。
3.特定类型方法的引用。
4.构造器引用。
1.静态方法的引用
引用格式:类名::静态方法。
静态方法引用的注意事项
重要:被引用的方法的参数列表要和函数式接口中的抽象方法的参数列表一致。
1
2
3
4
| // 使用静态方法进行简化!
Collections.sort(lists, (o1, o2) -> Student.compareByAge(o1 , o2));
// 如果前后参数是一样的,而且方法是静态方法,既可以使用静态方法引用
Collections.sort(lists, Student::compareByAge);
|
2.实例方法的引用
格式:对象::实例方法。
实例方法引用的注意事项
重要:被引用的方法的参数列表要和函数式接口中的抽象方法的参数列表一致。
1
2
3
4
5
| // 对象是 System.out = new PrintStream();
// 实例方法:println()
// 前后参数正好都是一个
lists.forEach(s -> System.out.println(s));
lists.forEach(System.out::println);
|
3.特定类型方法的引用
特定类型:String ,任何类型。
格式:特定类型::方法。
注意:如果第一个参数列表中的形参中的第一个参数作为了后面的方法的调用者,
并且其余参数作为后面方法的形参,那么就可以用特定类型方法引用了。
1
2
3
| Arrays.sort(strs, (s1, s2) -> s1.compareToIgnoreCase(s2));
// 特定类型的方法引用:
Arrays.sort(strs, String::compareToIgnoreCase);
|
4.构造器引用
格式是:类名::new
注意点:前后参数一致的情况下,又在创建对象就可以使用构造器引用
s -> new Student(s) => Student::new
小结:
方法引用是可遇不可求,能用则用,不能用就不要用!
1
2
| String[] strs1 = lists.toArray(s -> new String[s]);
String[] strs2 = lists.toArray(String[]::new);
|
Stream流#
目标:Stream流介绍。
什么是Stream流?
在Java8中,得益于Lambda所带来的函数式编程
引入了一个全新的Stream流概念,用于解决已有集合/数组类库有的弊端。
Stream流能解决什么问题?
可以解决已有集合类库或者数组API的弊端。
Stream认为集合和数组操作的API很不好用,所以采用了Stream流简化集合和数组的操作!!
小结:
Stream流是用来简化集合类库或者数组API的弊端。
Stream流其实就一根传送带,元素在上面可以被Stream流操作。
目标:Stream流的获取
Stream流式思想的核心:
是先得到集合或者数组的Stream流(就是一根传送带)
然后就用这个Stream流操作集合或者数组的元素。
然后用Stream流简化替代集合操作的API.
集合获取流的API:
(1) default Stream stream();
小结:
集合获取Stream流用: stream();
数组:Arrays.stream(数组) / Stream.of(数组);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| /** --------------------Collection集合获取流------------------------------- */
// Collection集合如何获取Stream流。
Collection<String> c = new ArrayList<>();
Stream<String> ss = c.stream();
/** --------------------Map集合获取流------------------------------- */
Map<String, Integer> map = new HashMap<>();
// 先获取键的Stream流。
Stream<String> keyss = map.keySet().stream();
// 在获取值的Stream流
Stream<Integer> valuess = map.values().stream();
// 获取键值对的Stream流(key=value: Map.Entry<String,Integer>)
Stream<Map.Entry<String,Integer>> keyAndValues = map.entrySet().stream();
/** ---------------------数组获取流------------------------------ */
// 数组也有Stream流。
String[] arrs = new String[]{"Java", "JavaEE" ,"Spring Boot"};
Stream<String> arrsSS1 = Arrays.stream(arrs);
Stream<String> arrsSS2 = Stream.of(arrs);
|
目标:Stream流的常用API
forEach : 逐一处理(遍历)
count:统计个数
– long count();
filter : 过滤元素
– Stream filter(Predicate<? super T> predicate)
limit : 取前几个元素
skip : 跳过前几个
map : 加工方法(把原来的元素加工以后,从新放上去)
– Stream map(Function<? super T, ? extends R> mapper);
concat : 合并流。
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
| List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");
list.add("张三丰");
list.stream().filter( s -> s.length() == 3 ).filter( s -> s.startsWith("张"))
.forEach( System.out::println);
// 统计数量
long count = list.stream().filter( s -> s.length() == 3 )
.filter( s -> s.startsWith("张")).count();
System.out.println(count);
// 取前2个
list.stream().filter(s -> s.length() == 3).limit(2)
.forEach( System.out::println);
// 跳过前2个
list.stream().filter(s -> s.length() == 3).skip(2)
.forEach( System.out::println);
// 需求:把名称都加上“黑马的:+xxx”
list.stream().map(s -> "黑马的:" + s).forEach(System.out::println);
// 需求:把名称都加工成学生对象放上去!!
//list.stream().map(name -> new Student(name)).forEach(name -> System.out.println(name));
list.stream().map(Student::new).forEach(System.out::println);
|
目标:终结与非终结方法。
终结方法:一旦Stream调用了终结方法,流的操作就全部终结了,不能继续使用,只能创建新的Stream操作。
终结方法: foreach , count。
非终结方法:每次调用完成以后返回一个新的流对象,可以继续使用,支持链式编程!
目标:收集Stream流: 把Stream流的数据转回成集合。
Stream的作用是:把集合转换成一根传送带,借用Stream流的强大功能进行的操作。但是实际开发中数据最终的形式还是
应该是集合,最终Stream流操作完毕以后还是要转换成集合。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");
list.add("张三丰");
Stream<String> zhangLists = list.stream().filter(s -> s.startsWith("张"));
// 把stream流转换成Set集合。
Set<String> sets = zhangLists.collect(Collectors.toSet());
// 把stream流转换成List集合。
Stream<String> zhangLists1 = list.stream().filter(s -> s.startsWith("张"));
List<String> lists = zhangLists1.collect(Collectors.toList());
// 把stream流转换成数组。
Stream<String> zhangLists2 = list.stream().filter(s -> s.startsWith("张"));
//Object[] arrs = zhangLists2.toArray();
// 可以借用构造器引用申明转换成的数组类型!!!
String[] arrs1 = zhangLists2.toArray(String[]::new);
|
File类#
目标:File类的概述和API
File类:代表操作系统的文件对象。是用来操作操作系统的文件对象的,删除文件,获取文件信息,创建文件(文件夹)
广义来说操作系统认为文件包含(文件和文件夹)
File类的创建文件对象的API:
包:java.io.File
(1)构造器:
– public File(String pathname):根据路径获取文件对象
– public File(String parent , String child):根据父路径和文件名称获取文件对象!
– public File(File parent , String child):根据父文件名和文件名称获取文件对象!
File类创建文件对象的格式:
a.File f = new File(“绝对路径/相对路径”);
绝对路径:从磁盘的的盘符一路走到目的位置的路径。
– 绝对路径依赖具体的环境,一旦脱离环境,代码可能出错!!
– 一般是定位某个操作系统中的某个文件对象。
相对路径:不带盘符的。(重点)
– 默认是直接相对到工程目录下寻找文件的。
– 相对路径只能用于寻找工程下的文件。
– 能用相对路径就应该尽量使用,可以跨平台!
b.File f = new File(“文件对象/文件夹对象”);
广义来说:文件是包含文件和文件夹的。
小结:
创建文件对象可以用绝对路径也可以用相对路径。
相对路径只能用于寻找工程下的文件。
文件对象可以表示文件也可以表示文件夹!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // 1.创建文件对象:使用绝对路径
// 文件路径分隔符:
// -- a.使用正斜杠: /
// -- b.使用反斜杠: \\
// -- c.使用分隔符API:File.separator
File f1 = new File("D:/itcast/图片资源/beautiful.jpg");
File f1 = new File("D:"+File.separator+"itcast"+File.separator+"图片资源"+File.separator+"beautiful.jpg");
File f1 = new File("D:\\itcast\\图片资源\\beautiful.jpg");
System.out.println(f1.length()); // 获取文件的大小,字节大小
// 2.创建文件对象:使用相对路径
File f2 = new File("Day09Demo/src/dlei01.txt");
System.out.println(f2.length());
// 3.创建文件对象:代表文件夹。创建一个文件对象去指向这个目录,并不是如果这个文件目录不存在会自动创建目录
File f3 = new File("D:\\itcast\\图片资源");
System.out.println(f3.exists());// 判断路径是否存在!!
|
目标:File类API
获取功能:
- public String getAbsolutePath() :返回此File的绝对路径名字符串。
- public String getPath() : 获取创建文件对象的时候用的路径
- 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表示的文件或目录。 (只能删除空目录)
- public boolean mkdir() :创建由此File表示的目录。(只能创建一级目录)
- public boolean mkdirs() :可以创建多级目录 (建议使用的)
遍历功能:
- public String[] list():获取当前目录下所有的"一级文件名称"到一个字符串数组中去返回。
- public File[] listFiles():获取当前目录下所有的"一级文件对象"到一个文件对象数组中去返回(重点)
递归三要素
1.递归的终结点: f(1) = 1
2.递归的公式:f(x) = f(x - 1) + 1
3.递归的方向:必须走向终结点
递归实现文件搜索
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
| public class FileSearchDemo {
public static void main(String[] args) {
// 搜索调用方法
searchFiles(new File("D:/soft") , "eclipse.exe");
}
/**
* 去某个目录下搜索某个文件
* @param dir 搜索文件的目录。
* @param fileName 搜索文件的名称。
*/
public static void searchFiles(File dir , String fileName){
// 1.判断是否存在该路径,是否是文件夹
if(dir.exists() && dir.isDirectory()){
// 2.提取当前目录下的全部一级文件对象
File[] files = dir.listFiles(); // null/[]
// 3.判断是否存在一级文件对象(判断是否不为空目录)
if(files!=null && files.length > 0){
// 4.判断一级文件对象
for (File f : files) {
// 5.判断file是文件还是文件夹
if(f.isFile()){
// 6.判断该文件是否为我要找的文件对象
if(f.getName().contains(fileName)){
System.out.println(f.getAbsolutePath());
try {
// 启动它(拓展)
Runtime r = Runtime.getRuntime();
r.exec(f.getAbsolutePath());
} catch (IOException e) {
e.printStackTrace();
}
}
}else{
// 7.该文件是文件夹,文件夹要递归进入继续寻找
searchFiles(f ,fileName);
}
}
}
}
}
}
|
字符集#
目标:字符集/编码集
字符集:各个国家为自己国家的字符取的一套编号规则。
计算机的底层是不能直接存储字符的。
计算机的底层只能存储二进制。010101
二进制就是可以转成10进制的。10进制就是整数编号。
101 = 1x2^0 + 0x2^1 + 1x2^2 = 5
结论:计算机的底层可以存储编号。
1B = 8b 计算机中的最小单位是字节B.
美国人:
8个开关一组就可以编码字符。 1个字节。
2^8 = 256
一个字节存储一个字符完全够用了。
a 97
b 98
A 65
B 66
0 48
1 49
这套编码是ASCII编码。
英文和数字在底层存储的时候都是采用1个字节存储的。
中国人:
中国人的字符很多:9万左右字符。
中国人一般采用2个字节编码一个中文字符。
这套编码叫:GBK编码。
它也必须兼容ASCII编码表。
….
美国人:
我来收集全球所有的字符,统一编号。这套编码叫 Unicode编码(万国码)
UTF-8就是变种形式。
UTF-8一个中文一般占3个字节。
它也必须兼容ASCII编码表。
小结:
英文和数字在任何编码集中都是一样的,都占1个字节。
GBK编码中,1个中文字符一般占2个字节。
UTF-8编码中,1个中文字符一般占3个字节。
技术人员都应该使用UTF-8编码!
编码前与编码后的编码集必须一致才不会乱码!!
GBK 我爱你[oo oo oo] GBK 不会乱码!
GBK 我爱你[oo oo oo] UTF-8 会乱码!
英文和数字在任何编码集中可以通用,不会乱码!!
IO流的概述和分类#
目标:IO流读写数据。
IO输入输出流:
Input:输入。
Output:输出。
引入:
File类只能操作文件对象本身,不能读写文件对象的内容。
读写数据内容,应该使用IO流。
IO流是一个水流模型:IO理解成水管,把数据理解成水流。
IO流的分类:
按照流的方向分为:输入流,输出流。
(1)输出流:以内存为基准,把内存中的数据写出到磁盘文件或者网络介质中去的流称为输出流。
输出流的作用:写数据到文件,或者写数据发送给别人。
(2)输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据读入到内存中去的流称为输入流。
输入流的作用:读取数据到内存。
按照流的内容分为: 字节流,字符流。
(1)字节流:流中的数据的最小单位是一个一个的字节,这个流就是字节流。
(2)字符流:流中的数据的最小单位是一个一个的字符,这个流就是字符流。(针对于文本内容)
所以流大体分为四大类:
字节输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据以一个一个的字节的形式读入到内存中去
字节输出流:以内存为基准,把内存中的数据以一个一个的字节写出到磁盘文件或者网络介质中去
字符输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据以一个一个的字符的形式读入到内存中去
字符输出流:以内存为基准,把内存中的数据以一个一个的字符写出到磁盘文件或者网络介质中去
字节流#
目标:字节输入流的使用。
IO流的体系:
字节流 字符流
字节输入流 字节输出流 字符输入流 字符输出流
InputStream OutputStream Reader Writer (抽象类)
FileInputStream FileOutputStream FileReader FileWriter (子类实现类)
a.FileInputStream文件字节输入流
– 作用:以内存为基准,把磁盘文件中的数据按照字节的形式读入到内存中的流。
简单来说,就是按照字节读取文件数据到内存。
– 构造器:
1.public FileInputStream(File path):创建一个字节输入流管道与源文件对象接通。
2.public FileInputStream(String pathName):创建一个字节输入流管道与文件路径对接。
– 方法:
1.public int read():每次读取一个字节返回!读取完毕会返回-1。
小结:
一个一个字节读取英文和数字没有问题。
但是一旦读取中文输出无法避免乱码,因为会截断中文的字节。
一个一个字节的读取数据,性能也较差,所以禁止使用此方案!
目标:字节输出流的使用
b.FileOutputStream文件字节输出流
– 作用:以内存为基准,把内存中的数据,按照字节的形式写出到磁盘文件中去。
– 构造器:
public FileOutputStream(File file): 创建一个字节输出流管道通向目标文件对象。
public FileOutputStream(String file): 创建一个字节输出流管道通向目标文件路径。
public FileOutputStream(File file , boolean append):创建一个追加数据的字节输出流管道通向目
标文件对象。
public FileOutputStream(String file , boolean append):创建一个追加数据的字节输出流管道通向
目标文件路径。
– 方法:
public void write(int a): 写一个字节出去。
public void write(byte[] buffer): 写一个字节数组出去。
public void write(byte[] buffer , int pos , int len):写一个字节数组的一部分出去。
参数一,字节数组;参数二:起始字节索引位置,参数三:写多少个字节数出去。
小结:
字节输出流只能写字节出去。
字节输出流默认是覆盖数据管道。从新开启管道后,覆盖文件内容。
换行用: os.write("\r\n”.getBytes());
关闭和刷新:刷新流可以继续使用,关闭包含刷新数据但是流就不能使用了!
FileInputStream:
1
2
3
4
5
6
7
8
9
10
| // 1.创建文件对象定位dlei01.txt
File file = new File("Day09Demo/src/dlei01.txt");
// 2.创建一个字节输入流管道与源文件接通
InputStream is = new FileInputStream(file);
// 4.使用while读取字节数
// 定义一个整数变量存储字节
int ch = 0;
while((ch = is.read())!= -1){
System.out.print((char) ch);
}
|
1
2
3
4
5
6
7
8
9
10
| InputStream is = new FileInputStream("Day09Demo/src/dlei02.txt");
// 读法优化,必须使用循环 // abc xyz i
// a.定义一个字节数组代表桶 // ooo ooo o
byte[] buffer = new byte[3];
int len ; // 存储每次读取的字节数。
while((len = is.read(buffer)) != -1){
// 读取了多少就倒出多少!
String rs = new String(buffer , 0 , len);
System.out.print(rs);
}
|
FileOutputStream:
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
| //简化写法 创建一个字节输出流管道与目标文件路径接通,默认是覆盖管道
OutputStream os = new FileOutputStream("Day09Demo\\src\\dlei01.txt");
//追加管道 从新开启管道后,在原文件内容的基础上追加内容。
//OutputStream os = new FileOutputStream("Day09Demo\\src\\dlei01.txt",true);
//写一个字节出去
os.write('a');//a
os.write(98); //ab
os.write('类'); // [ooo] 默认是UTF-8 中文占3个字节 只会写出中文的第一个字节,写出去就乱码
//写一个字节数组出去
byte[] bytes = new byte[]{98, 99, 100, 111};
os.write(bytes);
// \n 在winos系统可以换行, \r\n是为了兼容其他系统的换行
os.write("\r\n".getBytes());
//写出一个字符串
byte[] bytes1 = "java是最优美的语言".getBytes(); //默认以当前代码编码UTF-8提取字节数组
//byte[] bytes1 = "java是最优美的语言".getBytes("GBK"); //以编码GBK提取字节数组
os.write(bytes1);
os.write("\r\n".getBytes());
//写一个字节数组的一部分出去
byte[] bytes2 = "java是最优美的语言".getBytes(); //默认以当前代码编码UTF-8提取字节数组
os.write(bytes2, 0, 19); //java是最优美的
//os.flush(); //立即刷新数据到文件中去,刷新后管道可以继续使用
os.close(); //关闭资源管道,关闭包含了刷新,关闭后管道不能使用了
|
拓展:解决字节输入流读取中文内容输出乱码的问题。
引入:
一个一个字节读取中文输出
一个一个字节数组读取中文输出均无法避免乱码。
如何实现读取可以避免乱码呢?
1.定义一个字节数组与文件的大小刚刚一样大,然后一桶水读取全部字节数据再输出!
小结:
定义一个字节数组与文件的大小刚刚一样大,然后一桶水读取全部字节数据再输出!
可以避免中文读取输出乱码,但是如果读取的文件过大,会出现内存溢出!!
字节流并不适合读取文本文件内容输出,读写文件内容建议使用字符流。
1
2
3
4
5
6
7
| // 1.定位文件对象
File f = new File("Day09Demo/src/dlei03.txt");
InputStream is = new FileInputStream(f);
//readAllBytes() JDK1.9才可以使用
byte[] buffer = is.readAllBytes();
String rs = new String(buffer);
System.out.println(rs);
|
目标:字节流做文件复制。
字节流复制的思想:
字节是计算机中一切文件的组成,所以字节流适合做一切文件的复制。
复制是把源文件的全部字节一字不漏的转移到目标文件,只要文件前后的格式一样,绝对不会有问题。
需求:
原文件:D:\itcast\图片资源\meinv.jpg
目标文件:D:\itcast\meimei.jpg
分析步骤:
(1)创建一个字节输入流管道与源文件接通。
(2)创建一个字节输出流与目标文件接通。
(3)创建一个字节数组作为桶
(4)从字节输入流管道中读取数据,写出到字节输出流管道即可。
(5)关闭资源!
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
| InputStream is = null ;
OutputStream os = null ;
try{
/** (1)创建一个字节输入流管道与源文件接通。 */
is = new FileInputStream("D:\\itcast\\图片资源\\meinv.jpg");
/** (2)创建一个字节输出流与目标文件接通。*/
os = new FileOutputStream("D:\\itcast\\meimei.jpg");
/** (3)创建一个字节数组作为桶*/
byte[] buffer = new byte[1024];
/** (4)从字节输入流管道中读取数据,写出到字节输出流管道即可。*/
int len = 0;
while((len = is.read(buffer)) != -1){
// 读取多少就倒出多少
os.write(buffer, 0 , len);
}
System.out.println("复制完成!");
}catch (Exception e){
e.printStackTrace();
} finally {
/**(5)关闭资源! */
try{
if(os!=null)os.close();
if(is!=null)is.close();
}catch (Exception e){
e.printStackTrace();
}
}
|
目标:JDK 1.7开始之后释放资源的新方式
try-with-resources:
try(
// 这里只能放置资源对象,用完会自动调用close()关闭
){
}catch(Exception e){
e.printStackTrace();
}
什么是资源?
资源类一定是实现了Closeable接口,实现这个接口的类就是资源
有close()方法,try-with-resources会自动调用它的close()关闭资源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| try(
/** (1)创建一个字节输入流管道与源文件接通。 */
InputStream is = new FileInputStream("D:\\itcast\\图片资源\\meinv.jpg");
/** (2)创建一个字节输出流与目标文件接通。*/
OutputStream os = new FileOutputStream("D:\\itcast\\meimei.jpg");
/** (5)关闭资源!是自动进行的 */
){
/** (3)创建一个字节数组作为桶*/
byte[] buffer = new byte[1024];
/** (4)从字节输入流管道中读取数据,写出到字节输出流管道即可。*/
int len = 0;
while((len = is.read(buffer)) != -1){
// 读取多少就倒出多少
os.write(buffer, 0 , len);
}
System.out.println("复制完成!");
}catch (Exception e){
e.printStackTrace();
}
|
字符流#
目标:字符输入流的使用
字符流的体系:
字符输入流 字符输出流
Reader Writer (抽象类)
FileReader FileWriter (实现类)
FileReader文件字符输入流
– 作用:以内存为基准,把磁盘文件的数据以字符的形式读入到内存。
– 构造器:
public FileReader(File file):创建一个字符输入流与源文件对象接通。
public FileReader(String filePath):创建一个字符输入流与源文件路径接通。
– 方法:
public int read(): 读取一个字符的编号返回! 读取完毕返回-1
public int read(char[] buffer):读取一个字符数组,读取多少个字符就返回多少个数量,读取完
毕返回-1
小结:
字符流一个一个字符的读取文本内容输出,可以解决中文读取输出乱码的问题。
字符流很适合操作文本文件内容。
字节流适合操作音视频文件
但是:一个一个字符的读取文本内容性能较差!!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| Reader fr = new FileReader("Day10Demo/src/dlei01.txt");
// 5.while循环一个一个字符读取。
// 定义一个变量存储一个字符的编号
int ch ;
while ((ch = fr.read()) != -1){
System.out.print((char)ch);
}
// a.按照字符数组读取数据使用循环
char[] buffer = new char[1024]; // 1K
// b.定义一个整数记录每次桶读取的字符数据量。
int len;
while((len = fr.read(buffer)) != -1 ) {
// 读取多少倒出多少字符
System.out.print(new String(buffer, 0 , len));
}
fr.close();
|
目标:字符输出流的使用
FileWriter文件字符输出流
– 作用:以内存为基准,把内存中的数据按照字符的形式写出到磁盘文件中去。
– 构造器:
public FileWriter(File file):创建一个字符输出流管道通向目标文件对象。
public FileWriter(String filePath):创建一个字符输出流管道通向目标文件路径。
public FileWriter(File file,boolean append):创建一个追加数据的字符输出流管道通向目标文
件对象。
public FileWriter(String filePath,boolean append):创建一个追加数据的字符输出流管道通向
目标文件路径。
– 方法:
a.public void write(int c):写一个字符出去
b.public void write(String c)写一个字符串出去:
c.public void write(char[] buffer):写一个字符数组出去
d.public void write(String c ,int pos ,int len):写字符串的一部分出去
e.public void write(char[] buffer ,int pos ,int len):写字符数组的一部分出去
小结:
字符输出流可以写字符数据出去,总共有5个方法写字符。
覆盖管道:
Writer fw = new FileWriter(“Day10Demo/src/dlei03.txt”); // 覆盖数据管道
追加数据管道:
Writer fw = new FileWriter(“Day10Demo/src/dlei03.txt”,true); // 追加数据管道
换行:
fw.write("\r\n"); // 换行
读写字符文件数据建议使用字符流。
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
| // 1.创建一个字符输出流管道通向目标文件路径
//Writer fw = new FileWriter("Day10Demo/src/dlei03.txt"); // 覆盖数据管道
Writer fw = new FileWriter("Day10Demo/src/dlei03.txt",true); // 追加数据管道
// 2.写一个字符出去:public void write(int c):写一个字符出去
fw.write(97); // 字符a
fw.write('b'); // 字符b
fw.write('磊'); // 字符磊,此时没有任何问题。
fw.write("\r\n"); // 换行
// 3.写一个字符串出去:public void write(String c)写一个字符串出去:
fw.write("Java是最优美的语言!");
fw.write("我们在黑马学习它!");
fw.write("\r\n"); // 换行
// 4.写一个字符数组出去:public void write(char[] buffer):写一个字符数组出去
fw.write("我爱中国".toCharArray());
fw.write("\r\n"); // 换行
// 5.写字符串的一部分出去: public void write(String c ,int pos ,int len):写字符串的一部分出去
fw.write("Java是最优美的语言!",0,9);
fw.write("\r\n"); // 换行
// 6.写字符数组的一部分出去:public void write(char[] buffer ,int pos ,int len):写字符数组的一部分出去
fw.write("我爱中国".toCharArray(),0 ,2);
fw.write("\r\n"); // 换行
fw.close();
|
缓冲流#
目标:缓冲流的概述和分类
字节流 字符流
字节输入流 字节输出流 字符输入流 字符输出流
InputStream OutputStream Reader Writer (抽象类)
FileInputStream FileOutputStream FileReader FileWriter(实现类,低级流,原始流)
BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter(实现类,缓冲流)
什么是缓冲流:缓冲流可以提高字节流和字符流的读写数据的性能。
缓冲流分为四类:
(1)BufferedInputStream: 字节缓冲输入流,可以提高字节输入流读数据的性能。
(2)BufferedOutStream: 字节缓冲输出流,可以提高字节输出流写数据的性能。
(3)BufferedReader: 字符缓冲输入流,可以提高字符输入流读数据的性能。
(4)BufferedWriter: 字符缓冲输出流,可以提高字符输出流写数据的性能。
目标:字节缓冲输入流
字节缓冲输入流:BufferedInputStream
– 作用:可以把低级的字节输入流包装成一个高级的缓冲字节输入流管道,从而提高字节输入流读数据的性能。
– 构造器: public BufferedInputStream(InputStream in)
– 原理:缓冲字节输入流管道自带了一个8KB的缓冲池,每次可以直接借用操作系统的功能最多提取8KB的数据到缓冲池中去,以后我们直接从缓冲池读
取数据,所以性能较好!
小结:
字节缓冲输入流:BufferedInputStream
可以把低级的字节输入流包装成一个高级的缓冲字节输入流管道,从而提高字节输入流读数据的性能。
功能几乎无变化。
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 1.定义一个低级的字节输入流与源文件接通
InputStream is = new FileInputStream("Day10Demo/src/dlei04.txt");
// 3.把低级的字节输入流包装成一个高级的缓冲字节输入流。
BufferedInputStream bis = new BufferedInputStream(is);
// 2.定义一个字节数组按照循环读取。
byte[] buffer = new byte[3];
int len ;
while((len = is.read(buffer)) != -1){
String rs = new String(buffer, 0 , len);
System.out.print(rs);
}
|
目标:字节缓冲输出流
字节缓冲输出流:BufferedOutputStream
– 作用:可以把低级的字节输出流包装成一个高级的缓冲字节输出流,从而提高写数据的性能。
– 构造器:public BufferedOutputStream(OutputStream os)
– 原理:缓冲字节输出流自带了8KB缓冲池,数据就直接写入到缓冲池中去,性能极高了!
小结:
字节缓冲输出流可以把低级的字节输出流包装成一个高级的缓冲字节输出流,从而提高写数据的性能。
功能几乎不变。
1
2
3
4
5
6
7
8
9
10
| // 1.写一个原始的字节输出流
OutputStream os = new FileOutputStream("Day10Demo/src/dlei05.txt");
// 3.把低级的字节输出流包装成一个高级的缓冲字节输出流
BufferedOutputStream bos = new BufferedOutputStream(os);
// 2.写数据出去
bos.write('a');
bos.write(100);
bos.write('b');
bos.write("我爱中国".getBytes());
bos.close();
|
目标:字符缓冲输入流
字符缓冲输入流:BufferedReader
– 作用:字符缓冲输入流可以把字符输入流包装成一个高级的缓冲字符输入流,可以提高字符输入流读数据的性能。
– 构造器:public BufferedReader(Reader reader)
– 原理:缓冲字符输入流默认会有一个8K的字符缓冲池,可以提高读字符的性能。
– 缓冲字符输入流除了提高了字符输入流的读数据性能,
缓冲字符输入流还多了一个按照行读取数据的功能(重点):
public String readLine(): 读取一行数据返回,读取完毕返回null;
小结:
字符缓冲输入流可以把字符输入流包装成一个高级的缓冲字符输入流,
可以提高字符输入流读数据的性能。
除此之外多了一个按照行读取数据的功能:public String readLine(): 读取一行数据返回,读取完毕返回null;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // 1.定义一个原始的字符输入流读取源文件
Reader fr = new FileReader("Day10Demo/src/dlei06.txt");
// 3.把低级的字符输入流管道包装成一个高级的缓冲字符输入流管道
BufferedReader br = new BufferedReader(fr);
// 定义一个字符串变量存储每行数据
String line;
// 使用一个循环读取数据(经典代码)
while((line = br.readLine()) != null){
System.out.println(line);
}
// 2.定义一个字符数组循环读取
// char[] buffer = new char[1024];
// int len ;
// while((len = br.read(buffer)) != -1){
// System.out.println(new String(buffer , 0 , len));
// }
br.close();
|
目标:字符缓冲输出流的使用
字符缓冲输出流:BufferedWriter
– 作用:把字符输出流包装成一个高级的缓冲字符输出流,提高写字符数据的性能。
– 构造器:public BufferedWriter(Writer writer)
– 原理:高级的字符缓冲输出流多了一个8k的字符缓冲池,写数据性能极大提高了!
– 字符缓冲输出流除了提高字符输出流写数据的性能,还多了一个换行的特有功能:
public void newLine():新建一行。
小结:
缓冲字符输出流可以把低级的字符输出流进行包装。提高了写字符的性能。
多了一个换行的功能:public void newLine():新建一行。
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 1.定义一个低级的字符输出流写数据出去
Writer fw = new FileWriter("Day10Demo/src/dlei07.txt",true);
// 3.把低级的字符输出流包装成高级的缓冲字符输出流
BufferedWriter bw = new BufferedWriter(fw);
// 2.写字符输出
bw.write("我在黑马学IO流~~~~");
bw.newLine(); // 换行
bw.write("我在黑马学IO流~~~~");
bw.newLine();// 换行
bw.close();
|
目标:字符流不同编码读取乱码的问题。
引入:
我们之前用的代码编码和文件编码都是UTF-8编码,字符流读取没有出现乱码!!
字符流读取:
代码编码 文件编码 中文情况。
UTF-8 UTF-8 不乱码!
GBK GBK 不乱码!
UTF-8 GBK 乱码!
小结:
如果代码编码和读取的文件编码一致。字符流读取的时候不会乱码。
如果代码编码和读取的文件编码不一致。字符流读取的时候会乱码。
转换流#
目标:字符输入转换流InputStreamReader
字节流 字符流
字节输入流 字节输出流 字符输入流 字符输出流
InputStream OutputStream Reader Writer (抽象类)
FileInputStream FileOutputStream FileReader FileWriter(实现类)
BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter(实现类,缓冲流)
InputStreamReader OutputStreamWriter
字符输入转换流InputStreamReader:
– 作用:可以解决字符流读取不同编码乱码的问题。
可以把原始的字节流按照当前默认的代码编码转换成字符输入流。
也可以把原始的字节流按照指定编码转换成字符输入流
– 构造器:
public InputStreamReader(InputStream is):可以使用当前代码默认编码转换成字符流,几乎不用!
public InputStreamReader(InputStream is,String charset):可以指定编码把字节流转换成字符流
小结:
字符输入转换流可以把字节输入流按照默认编码转换成字符输入流。
– Reader isr = new InputStreamReader(is); // 使用当前代码默认编码UTF-8转换成字符流,几乎不用!
字符输入转换流也可以指定编码把字节输入流转换成字符输入流。
– Reader isr = new InputStreamReader(is,“GBK”); // 指定编码把字节流转换成字符流
字符输入转换流可以解决不同编码读取乱码的问题!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 代码:UTF-8 文件:GBK(ab我爱你: o o [oo] [oo] [oo])
// 1.提取GBK文件的原始字节流
InputStream is = new FileInputStream("D:\\itcast\\网络编程公开课\\Netty.txt");
// 2.把原始字节输入流通过转换流,转换成 字符输入转换流InputStreamReader
//Reader isr = new InputStreamReader(is); // 使用当前代码默认编码UTF-8转换成字符流,几乎不用!
Reader isr = new InputStreamReader(is,"GBK"); // 指定编码把字节流转换成字符流
// 3.包装成缓冲流
BufferedReader br = new BufferedReader(isr);
// 4.定义一个字符串变量存储每行数据
String line;
// 使用一个循环读取数据(经典代码)
while((line = br.readLine())!=null){
System.out.println(line);
}
|
字符输出转换流:OutputStreamWriter
– 作用:可以指定编码把字节输出流转换成字符输出流。
可以指定写出去的字符的编码。
– 构造器:
public OutputStreamWriter(OutputStream os): 用当前默认编码UTF-8把字节输出流转换成字符输出流
public OutputStreamWriter(OutputStream os , String charset): 指定编码把字节输出流转换成字符输出流
小结:
字符输出转换流可以指定编码把字节输出流转换成字符输出流。
从而实现指定写出去的字符编码!
1
2
3
4
5
6
7
8
| // 1.写一个字节输出流通向文件
OutputStream os = new FileOutputStream("Day10Demo/src/dlei07.txt");
// 2.把字节输出流转换成字符输出流。
// Writer fw = new OutputStreamWriter(os); // .把字节输出流按照默认编码UTF-8转换成字符输出流。
Writer fw = new OutputStreamWriter(os,"GBK"); // . 把字节输出流按照指定编码GBK转换成字符输出流。
fw.write("abc我是中国人");
fw.close();
|
序列化#
目标:对象序列化技术
对象序列化:就是把Java对象数据直接存储到文件中去。 对象 => 文件中
对象反序列化:就是把Java对象的文件数据恢复到Java对象中。 文件中 => 对象
字节流 字符流
字节输入流 字节输出流 字符输入流 字符输出流
InputStream OutputStream Reader Writer(抽象类)
FileInputStream FileOutputStream FileReader FileWriter(实现类)
BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter(实现类,缓冲流)
ObjectInputStream ObjectOutputStream InputStreamReader OutputStreamWriter
对象序列化流(对象字节输出流):ObjectOutputStream
– 作用:把内存中的Java对象数据保存到文件中去。
– 构造器: public ObjectOutputStream(OutputStream out)
– 序列化方法:public final void writeObject(Object obj)
注意:对象如果想参与序列化,对象必须实现序列化接口 implements Serializable ,否则序列化失败!
如果一个字段不想参数序列化:transient修饰该成员变量,它将不参与序列化!
加入序列版本号:private static final long serialVersionUID = 1L;
小结:
注意:对象如果想参与序列化,对象必须实现序列化接口 implements Serializable ,否则序列化失败!
对象序列化使用的流是对象字节输出流:ObjectOutputStream
对象反序列化(对象字节输入流):ObjectInputStream
– 作用:读取序列化的对象文件恢复到Java对象中。
– 构造器:public ObjectInputStream(InputStream is)
– 方法: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
| public class User implements Serializable {
// 加入序列版本号
private static final long serialVersionUID = 1L;
...
}
//序列化
// 1.创建User用户对象
User user = new User("tsgz","003197","铁扇公主");
// 2.创建低级的字节输出流通向目标文件
OutputStream os = new FileOutputStream("Day10Demo/src/obj.dat");
// 3.把低级的字节输出流包装成高级的对象字节输出流ObjectOutputStream
ObjectOutputStream oos = new ObjectOutputStream(os);
// 4.通过对象字节输出流序列化对象:
oos.writeObject(user);
// 6.释放资源
oos.close();
System.out.println("序列化对象成功~~~~");
//反序列化
// 1.定义一个低级的字节输入流通向源文件
InputStream is = new FileInputStream("Day10Demo/src/obj.dat");
// 2.把字节输入流包装成高的对象字节输入流
ObjectInputStream ois = new ObjectInputStream(is);
// 3.反序列化
User user = (User) ois.readObject();
System.out.println(user);
System.out.println("反序列化完成!");
|
打印流#
目标:打印流PrintStream / PrintWriter
打印流的作用:
1.可以方便,快速的写数据出去。
2.可以实现打印啥出去,就是啥出去。
打印流的构造器:
public PrintStream(OutputStream os):
public PrintStream(String filepath):
小结:
打印流可以方便,且高效的打印各种数据。
PrintStream不光可以打印数据,还可以写"字节数据"出去。
PrintWriter不光可以打印数据,还可以写"字符数据"出去。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // 1.打印流PrintStream
//OutputStream os = new FileOutputStream("Day10Demo/src/dlei08.txt");
//PrintStream ps = new PrintStream(os);
PrintStream ps = new PrintStream("Day10Demo/src/dlei08.txt");
ps.println(97); // 写97
ps.println(110); // 写110
ps.println("我在黑马快乐的调皮~~");
ps.println(99.8);
ps.println(false);
ps.println('徐');
// 写字节数据出去
ps.write("我爱你".getBytes());
ps.close();
|
目标:打印流改变输出的流向,重定向。
System:
public static void setOut(PrintStream out) :让系统的输出流向打印流。
1
2
3
4
5
6
7
8
| PrintStream ps = new PrintStream("Day10Demo/src/log.txt");
System.setOut(ps); // 让系统的输出流向打印流。
System.out.println("==itheima1==");
System.out.println("==itheima2==");
System.out.println("==itheima3==");
System.out.println("==itheima4==");
System.out.println("==itheima5==");
|
Properties属性集对象#
目标:Properties的概述和使用(框架底层使用,了解这个技术即可)。(保存数据到属性文件)
Properties:属性集对象。
其实就是一个Map集合。也就是一个键值对集合。但是我们一般不会当集合使用,因为有HashMap。
Properties核心作用:
Properties代表的是一个属性文件,可以把键值对的数据存入到一个属性文件中去。
属性文件:后缀是.properties结尾的文件,里面的内容都是 key=value。
大家在后期学的很多大型框架技术中,属性文件都是很重要的系统配置文件。
users.properties
admin=123456
dlei=dlei
Properties的方法:
– public Object setProperty(String key, String value) : 保存一对属性。
– public String getProperty(String key) :使用此属性列表中指定的键搜索属性值
– public Set stringPropertyNames() :所有键的名称的集合
– public void store(OutputStream out, String comments):保存数据到属性文件中去
– public void store(Writer fw, String comments):保存数据到属性文件中去
小结:
属性集对象Properties实际上是一个Map集合,可以实现把键值对数据保存到属性文件中去!!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // a.创建一个属性集对象:Properties的对象。
Properties properties = new Properties();
properties.setProperty("admin" , "123456");
properties.setProperty("dlei" , "101333");
// b.把属性集对象的数据存入到属性文件中去(重点)
OutputStream os = new FileOutputStream("Day10Demo/src/users.properties");
/**
* 参数一:被保存数据的输出管道
* 参数二:保存心得。就是对象保存的数据进行解释说明!
*/
properties.store(os , "i am very happy!!我快乐的保存了用户数据!");
// 1.创建一个属性集对象
Properties properties = new Properties();
// 2.字节输入流加载属性文件的数据到属性集对象properties中去。
properties.load(new FileInputStream("Day10Demo/src/users.properties"));
System.out.println(properties);
System.out.println(properties.getProperty("dlei"));
System.out.println(properties.getProperty("admin"));
|
CommonsIO包#
目标: Commons-io包的使用
什么是Commons-io包?
commons-io是apache开源基金组织提供的一组有关IO操作的类库
可以挺提高IO功能开发的效率。commons-io工具包提供了很多有关io操作的类
见下表:
包 功能描述
org.apache.commons.io 有关Streams、Readers、Writers、Files的工具类
org.apache.commons.io.input 输入流相关的实现类,包含Reader和InputStream
org.apache.commons.io.output 输出流相关的实现类,包含Writer和OutputStream
org.apache.commons.io.serialization 序列化相关的类
步骤:
1. 下载commons-io相关jar包;http://commons.apache.org/proper/commons-io/
2. 把commons-io-2.6.jar包复制到指定的Module的lib目录中
3. 将commons-io-2.6.jar加入到classpath中
小结:
IOUtils和FileUtils可以方便的复制文件和文件夹!!
1
2
3
4
5
6
7
8
9
| // 1.完成文件复制!
IOUtils.copy(new FileInputStream("Day13Demo/src/books.xml"), new FileOutputStream("Day13Demo/new.xml"));
// 2.完成文件复制到某个文件夹下!
FileUtils.copyFileToDirectory(new File("Day13Demo/src/books.xml"), new File("D:/itcast"));
// 3.完成文件夹复制到某个文件夹下!
FileUtils.copyDirectoryToDirectory(new File("D:\\itcast\\约吧图片服务器") , new File("D:\\"));
// Java从1.7开始提供了一些nio, 自己也有一行代码完成复制的技术。
Files.copy(Paths.get("Day13Demo/src/books.xml"), new FileOutputStream("Day13Demo/new11.txt"));
|
单元测试#
目标:单元测试的概念和操作步骤
单元测试是指程序员写的测试代码给自己的类中的方法进行预期正确性的验证。
单元测试一旦写好了这些测试代码,就可以一直使用,可以实现一定程度上的自动化测试。
单元测试一般要使用框架进行。
单元测试的经典框架:Junit
Junit是什么
- Junit是Java语言编写的第三方单元测试框架
- Junit框架的方案可以帮助我们方便且快速的测试我们的代码的正确性。
单元测试概念
- 单元:在Java中,一个类就是一个单元
- 单元测试:程序猿用Junit编写的一小段代码,用来对某个类中的某个方法进行功能测试或业务逻辑测试。
Junit单元测试框架的作用
- 用来对类中的方法功能进行有目的的测试,以保证程序的正确性和稳定性。
- 能够独立的测试某个方法或者所有方法的预期正确性。
Junit框架的使用步骤:
(1) 下载这个框架。(别人设计好的技术体系),框架一般是jar包的形式,jar包里面都是class文件(Java工程的最终形式),class文件就是我们调用
的核心代码。
现在不需要大家去官网下载,因为很多知名框架其实IDEA工具早就整合好了,程序员可以直接使用。
Junit已经被IDEA下载好了,可以直接导入到项目使用的。
(2)直接用Junit测试代码即可
a.先模拟业务代码
b.写测试类
测试类的命名规范:以Test开头,以业务类类名结尾,使用驼峰命名法
业务名称是:UserService
测试这个业务类的测试类:TestUserService/UserServiceTest
c.在测试类中写测试方法
测试方法的命名规则:以test开头,以业务方法名结尾
比如被测试业务方法名为:login,那么测试方法名就应该叫:testLogin
d.测试方法注意事项
必须是public修饰的,没有返回值,没有参数
必须使注解@Test修饰
(3)如何运行测试方法
选中方法名 –> 右键 –> Run ‘测试方法名’ 运行选中的测试方法
名 –> 右键 –> Run ‘测试类类名’ 运行测试类中所有测试方法
选中模块名 –> 右键 –> Run ‘All Tests’ 运行模块中的所有测试类的所有测试方法如何查看测试结果
绿色:表示测试通过
红色:表示测试失败,有问题
Junit常用注解(Junit 4.xxxx版本)
- @Test 测试方法!
- @Before:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次。
- @After:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次。
- @BeforeClass:用来静态修饰方法,该方法会在所有测试方法之前只执行一次。
- @AfterClass:用来静态修饰方法,该方法会在所有测试方法之后只执行一次。
开始执行的方法:初始化资源。
执行完之后的方法:释放资源。
Junit常用注解(Junit5.xxxx版本)
- @Test 测试方法!
- @BeforeEach:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次。
- @AfterEach:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次。
- @BeforeAll:用来静态修饰方法,该方法会在所有测试方法之前只执行一次。
- @AfterAll:用来静态修饰方法,该方法会在所有测试方法之后只执行一次。
目标:反射的概念
反射,注解,代理,泛型是Java的高级技术,是以后框架的底层原理必须使用到的技术。
反射:是Java独有的技术。是Java技术显著的特点。
反射是指对于任何一个类,在"运行的时候"都可以直接得到这个类全部成分。
在运行时,可以直接得到这个类的构造器对象。(Constructor)
在运行时,可以直接得到这个类的成员变量对象。(Field)
在运行时,可以直接得到这个类的成员方法对象。(Method)
反射的核心思想和关键: 就是得到编译以后的class文件对象。
反射提供了一个Class类型,就是可以得到编译以后的class类对象。
HelloWorld.java -> javac -> HelloWorld.class
Class c = HelloWorld.class;
注意:反射是工作在运行时的技术,因为只有运行之后才会有class类对象。
小结:
反射的核心思想和关键就是得到:编译以后的class文件对象
反射是在运行时获取类的字节码文件对象:然后可以解析类中的全部成分。
目标:反射获取Class对象
引入:
反射是通过先得到编译以后的Class类对象:字节码文件。然后才可以得到类中的全部成分,进行一些功能设计。
反射为一个类的全部成分都设计了一个类型来代表这个对象:
Class : 字节码文件的类型
Constructor : 构造器的类型
Field : 成员变量的类型
Method : 方法的类型
反射技术的第一步永远是先得到Class类对象:有三种方式获取
(1) 类名.class
(2) 通过类的对象.getClass()方法
(3) Class.forName(“类的全限名”)
– public static Class forName(String className)
Class类下的方法:
String getSimpleName(); 获得类名字符串:类名
String getName(); 获得类全名:包名+类名
T newInstance() ; 创建Class对象关联类的对象,其实底层也是调用无参数构造器,已经被淘汰。
小结:
Class类对象的获取有三种方式:
1.类名.class。
2.通过类的对象.getClass()方法。
3.Class.forName(“类的全限名”)。
Class类的方法:
String getSimpleName(); 获得类名字符串:类名
String getName(); 获得类全名:包名+类名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // 反射的第一步永远是先得到类的Class文件对象: 字节码文件。
// 1.类名.class
Class c1 = Student.class;
System.out.println(c1);
// 2.对象.getClass()
Student swk = new Student();
Class c2 = swk.getClass();
System.out.println(c2);
// 3.Class.forName("类的全限名")
// 直接去加载该类的class文件。
Class c3 = Class.forName("com.itheima._03反射_获取Class类对象.Student");
System.out.println(c3);
System.out.println(c1.getSimpleName()); // 获取类名本身(简名)
System.out.println(c1.getName()); // 获取类的全限名
// Student s1 = (Student) c1.newInstance(); // 调用无参数构造器得到对象,被淘汰了!
|
目标: 反射获取Constructor构造器对象
反射获取Class中的构造器对象Constructor作用:也是初始化并得到类的一个对象返回。
Constructor的API:
1. T newInstance(Object... initargs)建对象,注入构造器需要的数据。
2. void setAccessible(true)修改访问权限,true代表暴力攻破权限,false表示保留不可访问权限(暴力反射)
反射中Class类型获取构造器提供了很多的API:
Constructor getConstructor(Class… parameterTypes)
根据参数匹配获取某个构造器,只能拿public修饰的构造器,几乎不用!
Constructor getDeclaredConstructor(Class… parameterTypes)
根据参数匹配获取某个构造器,只要申明就可以定位,不关心权限修饰符,建议使用!
Constructor[] getConstructors()
获取所有的构造器,只能拿public修饰的构造器。几乎不用!!太弱了!
Constructor[] getDeclaredConstructors()
获取所有申明的构造器,只要你写我就能拿到,无所谓权限。建议使用!!
小结:
获取类的全部构造器对象: Constructor[] getDeclaredConstructors() 获取所有申明的构造器,只要你写我就能拿到,无所谓权限。建议使用!!
获取类的某个构造器对象:Constructor getDeclaredConstructor(Class… parameterTypes) 根据参数匹配获取某个构造器,只要申明就可以定位,
不关心权限修饰符,建议使用!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // 1.调用无参数构造器得到一个类的对象返回。
// a.反射第一步是先得到Class类对象
Class c = Student.class ;
// b.定位无参数构造器对象
Constructor constructor = c.getDeclaredConstructor();
// c.暴力打开私有构造器的访问权限
constructor.setAccessible(true);
// d.通过无参数构造器初始化对象返回
Student swk = (Student) constructor.newInstance(); // 最终还是调用无参数构造器的!
System.out.println(swk);
// 2.调用有参数构造器得到一个类的对象返回。
// a.反射第一步是先得到Class类对象
Class c = Student.class ;
// b.定位有参数构造器对象
Constructor constructor = c.getDeclaredConstructor(String.class , int.class);
// c.通过有参数构造器初始化对象返回
Student swk = (Student) constructor.newInstance("孙悟空",10000); // 最终还是调用有参数构造器的!
System.out.println(swk);
|
目标:反射获取Field成员变量对象
1、Field getField(String name);
根据成员变量名获得对应Field对象,只能获得public修饰
2.Field getDeclaredField(String name);
根据成员变量名获得对应Field对象,只要申明了就可以得到
3.Field[] getFields();
获得所有的成员变量对应的Field对象,只能获得public的
4.Field[] getDeclaredFields();
获得所有的成员变量对应的Field对象,只要申明了就可以得到
Field的方法:给成员变量赋值和取值
void set(Object obj, Object value):给对象注入某个成员变量数据
Object get(Object obj): 获取对象的成员变量的值。
void setAccessible(true):暴力反射,设置为可以直接访问私有类型的属性。
Class getType():获取属性的类型,返回Class对象。
String getName():获取属性的名称。
小结:
获取全部成员变量:getDeclaredFields
获取某个成员变量:getDeclaredField
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // a.先获取class类对象
Class c = Dog.class;
// b.获取全部申明的成员变量对象
Field[] fields = c.getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getName()+"===>"+field.getType());
}
// a.先获取class类对象
Class c = Dog.class;
// b.定位某个成员变量对象 :根据名称定位!!
Field ageF = c.getDeclaredField("age");
// c.为这个对象成员变量赋值
Dog taiDi = new Dog();
ageF.setAccessible(true);//暴力反射
/**
* 参数一:被赋值的对象。
* 参数二:该成员变量的值。
*/
ageF.set(taiDi , 5);
// d.获取成员变量的值
int value = ageF.get(taiDi);
|
目标:反射获取Method方法对象
反射获取类的Method方法对象:
1、Method getMethod(String name,Class…args);
根据方法名和参数类型获得对应的方法对象,只能获得public的
2、Method getDeclaredMethod(String name,Class…args);
根据方法名和参数类型获得对应的方法对象,包括private的
3、Method[] getMethods();
获得类中的所有成员方法对象,返回数组,只能获得public修饰的且包含父类的
4、Method[] getDeclaredMethods();
获得类中的所有成员方法对象,返回数组,只获得本类申明的方法。
Method的方法执行:
Object invoke(Object obj, Object… args)
参数一: 触发的是哪个对象的方法执行。
参数二: args:调用方法时传递的实际参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // a.先获取class类对象
Class c = Dog.class;
// b.定位它的某个方法
Method run = c.getDeclaredMethod("run");
// c.触发方法执行!
Dog jinMao = new Dog();
Object rs = run.invoke(jinMao); // 触发jinMao对象的run()方法执行!
System.out.println(rs);// 如果方法没有返回值,结果是null
/**
* 参数一:方法名称
* 参数二:方法的参数个数和类型(可变参数!)
*/
Method eat = c.getDeclaredMethod("eat",String.class);
eat.setAccessible(true); // 暴力反射!
/**
* 参数一:被触发方法所在的对象
* 参数二:方法需要的入参值
*/
Object rs1 = eat.invoke(jinMao,"肉");
System.out.println(rs1);// 如果方法没有返回值,结果是null
|
拓展:反射攻击集合泛型
1.反射可以破坏面向对象的封装性(暴力反射)。
2.同时可以破坏泛型的约束性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 泛型只能工作在编译阶段,运行阶段泛型就消失了,
// 反射工作在运行时阶段。
List<Double> scores = new ArrayList<>();
scores.add(99.3);
scores.add(199.3);
scores.add(89.5);
// 拓展:通过反射暴力的注入一个其他类型的数据进去。
// a.先得到集合对象的Class文件对象
Class c = scores.getClass();
// b.从ArrayList的Class对象中定位add方法
Method add = c.getDeclaredMethod("add", Object.class);
// c.触发scores集合对象中的add执行(运行阶段,泛型不能约束了)
add.invoke(scores,"波仔");
|