JavaSE
一、面向对象编程
hang in there ,thinks will work out
1、继承
-
extends
-
一个类只可以继承一个父类
-
java不支持多继承
-
继承之后可以使用 “ . ” 的方式访问父类中的非 private 的成员变量和方法
-
继承的使用方法
-
//Dog.class 子类 // 狗(子类) 继承 动物(父类) public class Dog extends Animal {}
-
继承父类后访问父类中的非 private 的成员变量和方法
-
// AnimalDemo.class 测试类 public class AnimalDemo { public static void main(String[] args) { // new 狗类 Dog dog = new Dog(); // 因为 Dog 继承了 Animal 父类 // 所以Dog可以访问父类中的非 private 成员变量和方法 dog.eat(); // 因为父类中的name 具有权限修饰符private 所以无法访问到name // dog.name; } }
//Animal.class 父类 public class Animal { private String name; private int age; public void eat(){ System.out.println("这是父类的 吃方法"); } }
-
构造方法
-
子类在继承了父类的时候,其子类的构造方法的第一行都会先执行父类的无参构造方法,若父类没有无参构造方法,则可以在子类的构造方法中,直接使用super(参数), 调用父类的有参构造方法;
若,父类中有无参构造方法,则子类的构造方法默认第一行都为super() , 即 : 第一行调用的都是父类的无参、
-
若父类,有有参构造方法,若不重写子类的无参构造方法,则会报错,因为子类的无参构造方法,默认第一行执行的是父类的无参构造方法,而现在,父类中没有无参构造方法,那么子类不能调用父类的无参构造方法,就必须重写子类的无参构造方法,将第一行改为super( 参数 ) , 这里的参数与父类中的有参构造函数的参数列表相对应
-
// StudentParent.class 父类 public class StudentParent { private int age; // 父类的无参构造方法 // public StudentParent() { // System.out.println("这是父类中的无参构造方法"); // } // 父类的有参构造方法 public StudentParent(int age) { this.age = age; } }
// Student.class 子类 public class Student extends StudentParent { private String name; // 子类的无参构造方法 public Student() { super(20); // 父类中没有 无参构造方法, 可以这样直接调用父类的有参构造方法 System.out.println("子类中的无参构造方法"); } // 子类的有参构造方法 public Student(int age) { super(age); } }
-
-
super 的使用方法
-
子类访问父类中的 非 private 和 static 成员变量、成员方法 和 构造函数 可以使用super 关键字访问
-
这样子访问成员变量需要用一个变量去承接他
-
super.age
-
public void show(){ int age1 = super.age; System.out.println("父类中的age"+ super.age); }
-
-
在一个方法中访问本类中的成员变量 可以使用 关键字 this
this.age
-
2、方法的重写与重载
-
Override 重写
-
方法重写的规则
1、参数列表与被重写的参数列表一致
2、返回值不可以修改
3、访问权限不能比父类中被重写方法的权限低
4、父类中的方法只能被他的子类重写
5、父类中被 private 和static 修饰的方法,不能被子类重写
-
子类继承父类后,可以重写父类中的方法
-
//Dog.class 子类 // 狗(子类) 继承 动物(父类) public class Dog extends Animal { // 子类重写父类中的eat方法 @Override public void eat(){ System.out.println("这是狗类的 吃 方法"); } }
-
-
Overload 重载
-
方法重载的条件
1、多个方法在同一个类中
2、多个方法的名相同
3、多个方法传递的参数的类型不同或个数不同
-
例如某个类中的构造方法
-
// StudentParent.class public class StudentParent { public int age = 20; public void eat(){ System.out.println("这是学生的吃方法"); } // 无参构造方法 public StudentParent() { System.out.println("这是父类中的无参构造方法"); } // 有参构造方法 public StudentParent(int age) { this.age = age; } }
-
3、多态
-
多态的基本条件
1、有继承或者实现的关系
2、方法的重写 – 子类重写父类中的方法
3、有父类的引用指向子类 (就是使用父类的类型的变量 承接 子类的实例对象)
-
//Demo.calss 测试类 public class Demo { public static void main(String[] args) { // new 狗类对象 Dog dog = new Dog(); dog.eat(); // 【结果】: 这是子类(狗类)重写的父类的吃方法 // 父类的引用指向子类对象 多态访问子类的方法 Animal animal = new Dog(); animal.eat(); // 这里就是父类的引用指向子类 , 这里的eat() 方法,来自于子类对父类eat方法的重写 // 【结果】: 这是子类(狗类)重写的父类的吃方法 } }
//Animal.class 父类 public class Animal { public int age = 40; // 父类的吃方法 public void eat(){ System.out.println("这是父类的 吃方法"); } }
// Dog.class 子类 // 狗(子类) 继承 动物(父类) public class Dog extends Animal { public int age = 20; public int weight = 100; // 子类重写父类中的eat方法 @Override public void eat(){ System.out.println("这是子类(狗类)重写的父类的吃方法"); } }
-
多态的访问特点
-
成员变量: 编译看左边,执行看左边;
-
成员方法:编译看左边,执行看右边; (因为执行的成员方法,可以被子类重写,所以看new Dog() 里是否重写了此成员方法)
-
为什么成员变量和成员方法的访问不一样呢?
因为成员方法有方法的重写,而成员方法没有。
-
-
多态访问成员变量
//Demo.class 测试类 public class Demo { public static void main(String[] args) { // 父类的引用指向子类对象 多态 Animal animal = new Dog(); // 左边 // 右边 System.out.println("animal.age = " + animal.age); // 【结果】 animal.age = 40 // 【原因】 这就是父类中的age,因为多态访问成员变量,编译看左边,执行看左边; System.out.println("animal.weigth = " + animal.weigth); // 【结果】 编译期报错 // 【原因】 因为 多态访问成员变量,编译看左边,执行看左边;所以Animal中没有weight这个成员变量,所以编译期报错了 } }
- 多态访问成员方法
// Demo.class 测试类 public class Demo { public static void main(String[] args) { // 父类的引用指向子类对象 多态 Animal animal = new Dog(); // 左边 // 右边 animal.eat(); // 【结果】 这是子类(狗类)重写的父类的吃方法 // 【原因】 成功! 因为父类和子类中都有eat() animal.show(); // 【结果】 编译期报错 // 【原因】 多态访问子类的成员方法,编译看左边,执行看右边;因为编译期中,看左边,但Animal中并没有show() , 所以会报错 } }
-
-
使用多态机制的优点:1、提高程序的扩展性
- 具体体现:定义方法的时候,使用父类类型作为参数,将来在使用的时候,使用具体的子类类型作为参数传入
-
多态机制的弊端:不能使用子类独有的方法, 即 : 父类没有的方法
-
代码实例:
//AnimalDemo.class 测试类
public class AnimalDemo {
public static void main(String[] args) {
// new 动物操作类
AnimalOperate animalOperate = new AnimalOperate();
// 调用动物的吃饭动作 未使用多态的机制
animalOperate.userCatEat(new Cat());
animalOperate.userDogEat(new Dog());
// 【结果】 我是猫类, 我正在吃饭 ...
// 【结果】 我是狗类, 我正在吃饭 ...
// 调用动物的吃饭动作 未使用多态的机制
animalOperate.userAnimalEat(new Cat());
animalOperate.userAnimalEat(new Dog());
// 【结果】 我是猫类, 我正在吃饭 ...
// 【结果】 我是狗类, 我正在吃饭 ...
}
}
//AnimalOperate.class 操作类
public class AnimalOperate {
// 未使用多态机制
public void userDogEat(Dog dog){
dog.eat();
}
public void userCatEat(Cat cat){
cat.eat();
}
// 使用多态机制
public void userAnimalEat(AnimalParent animalParent){
animalParent.eat();
}
}
//Cat.class 子类
public class Cat extends AnimalParent {
@Override
public void eat() {
System.out.println("我是猫类, 我正在吃饭 ... ");
}
}
//Dog.class 子类
public class Dog extends AnimalParent {
@Override
public void eat() {
System.out.println("我是狗类, 我正在吃饭 ... ");
}
}
//AnimalParent.class 父类
public class AnimalParent {
public void eat(){
System.out.println("我是AnimalParent 父类的吃方法");
}
}
- 多态的转型
// AnimalDemo02.class 测试类
public class AnimalDemo02 {
public static void main(String[] args) {
// new 狗类
Dog dog = new Dog();
dog.showDog();
// 使用多态机制
AnimalParent animalParent = new Dog();
animalParent.showDog();
// 【结果】 编译期报错
// 【原因】 父类AnimalParent中没有showDog方法,因为多态访问成员方法,编译看左边,执行看右边
// 将animalParent进行强制转换位Dog类型
Dog animalParent2 = (Dog) animalParent;
animalParent2.showDog();
// 【结果】 这是狗类的show方法
// 【原因】 因为animalParent强制转换成了Dog类型,且Dog类中有showDog方法,所以由于多态访问成员方法,编译看左边,执行看右边, 最后执行成功!
}
}
// Dog.class 子类
public class Dog extends AnimalParent {
@Override
public void eat() {
System.out.println("我是狗类, 我正在吃饭 ... ");
}
public void showDog(){
System.out.println("这是狗类的show方法");
}
}
// AnimalParent.class 父类
public class AnimalParent {
public void eat(){
System.out.println("我是AnimalParent 父类的吃方法");
}
}
4、抽象类
-
抽象类:例如 动物类是一个抽象的概念,没有确切具体的事物
-
关键字 abstarct :如果在方法加上abstarc关键字进行修饰,那么这个方法就为抽象方法;若有个类中存在一个抽象方法,那么这个类就称之为抽象类,抽象类也需要用abstarct进行修饰。
-
abstarct 不能与哪些关键字共存
关键字 能否共存 原因 private 否,相互冲突 abstarct 必须被重写,而private不能被继承 final 否,相互冲突 final修饰的方法,变量都不能修改,而abstact修饰的方法必须被重写 static 否,无意义 static是静态的, 被abstarct 修饰的方法,是没有方法体的
-
-
关键字 final :若给变量加上了final关键字修饰,并赋值,则这个变量为常量,且不可修改的
- 若子类继承的父类为抽象类,那么子类必须重写父类中的抽象方法
-
抽象类的特点
- 抽象类中不一定有抽象方法,但有抽象方法的类必须为抽象类
- 抽象类不能实例化, 也就是不能通过new 出来
- 抽象类由具体的子类进行实例化,也就是子类重写父类中的抽象方法
- 子类必须对父类中的所有抽象方法进行重写
- 在抽象类中可以定义非抽象方法
- 子类不重写抽象类中的方法,那么该子类仍然为抽象类
- 抽象类中可以有构造方法,用于子类访问父类时的数据初始化
// Animal.class
//因为类中存在抽象方法,所以此类也需要用abstarct 修饰
public abstract class Animal {
// 在这里我们定义一个吃方法,没有方法体, 因为没有方法体,所以需要用abstarct修饰
public abstract void eat();
}
// Dog.class
public class Dog extends Animal {
@Override
public void eat() {
System.out.println("我是狗类, 这是重写父类的吃方法。");
}
}
5、封装
- 封装:也就是将一个客观的事物封装成抽象类,并且类可以把自己的属性和方法只让可信的类或对象操作,对不可信的类进行隐藏。
- 封装类的规则
- 将类的某些信息隐藏在内部,不允许外部的程序直接访问
- 通过该类提供的方法实现对隐藏信息的操作与访问
- 封装的实现:
- 修改属性的私有信(使用private关键词修饰,只能被本类内部使用)
- 创建属性的getter/setter方法并设置为public,用于属性的读写
- 在getter/setter方法中加入属性控制语句,(对属性的赋值合理性进行判断)
// Jack.class 测试类
// Jack 是一个学生,就需要继承学生的基类
public class Jack {
public static void main(String[] args) {
Student student = new Student();
// 1、设置学生姓名
student.setName("Jack");
System.out.println("学生姓名"+student.getName());
// 【结果】 学生姓名Jack
// 2、设置学生虚假年龄
student.setAge(200);
System.out.println("学生年龄"+student.getAge());
// 【结果】 学生年龄0
// 【原因】 因为200属于不合理的年龄。所以赋值操作失败
// 3、设置学生真实年龄
student.setAge(20);
System.out.println("学生年龄"+student.getAge());
// 【结果】 学生年龄20
}
}
// Student.class 封装学生类
public class Student {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
// 对age 的赋值操作进行合理性判断
// 如果学生的年龄大于120岁,则认为不合理,就不执行age的赋值操作
if(age > 120) return;
this.age = age;
}
}
6、接口
- 接口的特性
- 没有方法体
- 实现此接口的类必须重写接口中的所有方法,(若子类不重写接口中的方法,就在子类class前加上 abstract )
- 接口是不可以被实例化的(也就是不能像对象一样被 new 出来 )
- 接口中的定义的成员变量(只能是常量)默认是加上public static final关键字的,接口中的成员变量是不可以修改的 (直接通过接口的名称访问即可)
- 在jdk8之前是不可以在接口中定义非抽象方法的,但在jdk8之后可以定义非抽象方法,但必须加上 default 关键字修饰
- 接口中没有构造方法,但是抽象类中是有构造方法的
- 实现类 先执行Object中的无参构造方法,在执行实现类中的无参构造方法
- 接口使用多态的特性
7、枚举
-
枚举类的作用
- 枚举用于做信息标志,信息父类。
-
枚举类的特点:
- 枚举类是final修饰的,不能被继承
- 枚举类默认继承了枚举类型:java.lang.Enum.
- 枚举的第一行列出的是枚举的对象,而且是用常量保存的,所以枚举类的第一行写的都是常量的名称,默认储存了枚举对象
- 枚举类的构造器是私有的,在非枚举类中是无法使用的
- 枚举类相当于是多利设计模式,。。。对象相当于是单例设计模式
-
枚举类的格式
-
权限修饰符 enum 枚举类名称{ 名称1,名称2, …}
-
package javaSE.面向对象编程.枚举; public class EnumDemo01 { public static void main(String[] args) { Sex s1 = Sex.BOY; System.out.println("s1 = " + s1); System.out.println("s1.ordinal() = " + s1.ordinal()); Sex s2 = Sex.GIRL; System.out.println("s2 = " + s2); System.out.println("s2.ordinal() = " + s2.ordinal()); // ordinal()方法,来自于父类-枚举类型, 可以获取枚举类的索引 } } public enum Sex{ BOY, GIRL; }
-
8、包
9、 内部类
-
内部类的特性
- 内部类可以直接访问外部类的成员属性(包括私有的)
- 外部类中要访问内部类中的成员属性,就必须new出内部类
-
注意
- 通常在企业中,外界是不可以访问内部类的,所以使用private来修饰内部类 保证数据的安全性
-
成员内部类,外界怎么访问呢?
-
格式:外部类名.内部类名 对象名 = 外部类对象.内部类对象
-
范例:
MayiktA.MayiktB mayiktB = new MayiktA().new MayiktB();
-
package javaSE.面向对象编程.内部类.外界访问内部类; // 外部类 public class MayiktA { // 内部类 public class MayiktB {} }
package javaSE.面向对象编程.内部类.外界访问内部类; // 测试类 public class Test { public static void main(String[] args) { // 访问MayiktA 的内部类MayiktB MayiktA.MayiktB mayiktB = new MayiktA().new MayiktB(); } }
-
静态内部类,外界如何访问?
- 外界如果访问静态的内部类中,直接 new 外部类.内部类( )
- 若静态内部类需要访问外部类的成员属性,则此成员必须被static修饰
package javaSE.面向对象编程.内部类.静态内部类; public class MayiktA { private String name; private static int age; public static void show(){ System.out.println("这是外部类的show方法"); } public static class MayiktB{ public static void main(String[] args) { System.out.println(name); // 【报错】 这里的name是爆红的,因为静态内部类只能访问外部类中被static修饰的成员属性 System.out.println(age); // 【结果】 这里的就可以访问age,因为age被static关键字修饰了 show(); // 【结果】 这里的就可以访问show方法,因为show方法被static关键字修饰了 } } }
-
局部内部类
- 在方法中定义的内部类为局部内部类, 该局部内部类只能在此方法中使用,此方法外面无法使用
package javaSE.面向对象编程.内部类.局部内部类; //外部类 public class MayiktA { // 外部方法 public void show(){ // 局部内部类 class MayiktB { public void test(){ System.out.println("这是show方法中的局部内部类中的test方法"); } } // 局部内部类只能在该方法中使用,方法外面无法使用 MayiktB mayiktB = new MayiktB(); } MayiktB mayiktB = new MayiktB(); // 【报错】 这里的MayiktB是爆红的,因为该类在show方法中,所以是局部内部类,只能在show方法中使用。 }
-
匿名内部类
- new出抽象类或者接口,然后 重写方法
- 要知道,接口和抽象类是不能够实例化的,但是匿名内部类中可以new出接口和抽象类,是因为匿名内部类的底层帮你创建了 实现类, 其名称是 null 的 —– 这是在编译阶段实现的
package javaSE.面向对象编程.内部类.匿名内部类; //外部类 public class Test01 { public static void main(String[] args) { // 这里的new出来的接口是因为匿名内部类底层帮助创建了实现类 AnimalParent dog = new AnimalParent() { @Override public void eat() { System.out.println("我是 dog, 这是我的吃方法"); } }; dog.eat(); } }
-
10、API
- api是jdk底层封装好的,可以直接拿来用
11、 关键字
- static (静态)
- 被static 修饰的成员变量和方法,被类的所有对象共享访问,静态修饰的访问特点:直接类名 . 成员变量 / 方法
- static 修饰成员变量:
- 被其修饰的成员变量,在类被同时实例化多次时,所有的实例化中的这个成员变量的指向都是同一个
- 使用static 修饰的常量,也可以直接使用 类名 . 常量名的形式访问
- static 修饰成员方法:
- 在同一个类中
- 静态方法:可以访问本类中的静态方法和静态成员变量,但是不可以访问非静态方法和非静态成员变量
- 非静态方法:可以访问本类中的静态方法和静态成员变量,也可以访问非静态方法和非静态成员变量
- 在同一个类中
- abstract (抽象的)
- 被此修饰符修饰的方法,被子类继承的时候,都要被子类重写
- 详解,请跳转至抽象类
- final(定义常量)
- 被该关键字修饰的基本数据类型 , 定义好之后是不可以被修改的
- 通常用来定义常量
public final int code = 200
12、Object
-
toString方法
- 若打印对象时,不重写toString方法,那么输出的是内存地址(hashCode)
- 重写toString方法之后,就可以返回对象的字符串形式
//重写toString方法 @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; }
-
equals方法
-
对象比较
-
若未重写equals方法,默认调用的是Object父类的equals方法,比较的是两个对象的内存地址
-
重写equals方法后,比较的是对象的每个值是否相等
-
// 重写equals方法 @Override public boolean equals(Object o) { // 判断两个对象的内存地址是否相同 if (this == o) return true; // 判断两个对象的类型是否相同,不相同直接返回false if (!(o instanceof Student)) return false; // 如果两个对象的类型相同就将传入的值强转为Student类型 Student student = (Student) o; // 最后在每个值进行 一 一 比较 return age == student.age && Objects.equals(name, student.name); }
-
-
字符串比较
-
equals()比较字符串就不需要重写了
-
//底层源码 public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
-
-
13、泛型
-
什么是泛型?
- 泛型就是一个标签:<数据类型>
- 泛型可以在编译阶段约束只能操作某种类型
-
注意:JDK1.7之后,泛型后面的声名可以省略不写
-
泛型和集合都只能支持引用数据类型,不支持基本数据类型,(若使用基本数据类型,则会自动进行包装整引用数据类型)
-
// 泛型就是在编译阶段约束只能操作某种数据类型 // 后期,可以通过反射向泛型里面添加别的类型 package javaSE.高级教程.泛型; import java.util.ArrayList; public class Demo01 { public static void main(String[] args) { // 这时候不使用泛型 ArrayList list = new ArrayList(); // 在编译期没有约束,任何类型都可以往里面进行添加 list.add("Java"); list.add(true); list.add(1); list.add(null); System.out.println("list = " + list); // 【结果】 list = [Java, true, 1, null] // 使用泛型之后 ArrayList<String> list1 = new ArrayList<>(); list1.add("Mysql"); // list1.add(true); // 就不能添加除String类型之外的类型 } }
-
泛型接口:核心思想=> 在实现接口的时候传入真实的数据类型,这样重写的方法就是对该数据类型进行操作
-
package javaSE.高级教程.泛型.接口泛型; //这里使用泛型的好处就是不用写死Demo02的数据类型, 这样就可以重复使用这个接口 public interface Demo02<E> { void add(E stu); void delete(E stu); void update(E stu); E query(int id); }
package javaSE.高级教程.泛型.接口泛型; //这里使用泛型,然后下面的参数类型就都为Student类型了 public class Demo02Impl implements Demo02<Student> { @Override public void add(Student stu) { } @Override public void delete(Student stu) { } @Override public void update(Student stu) { } @Override public Student query(int id) { return null; } }
-
通配符:
-
E , T , K , V , 可以在定义泛型的时候代表一切类型
-
? 可以在使用泛型的时候代表一切类型
-
package javaSE.高级教程.泛型.通配符; // 所有汽车一起比赛的一个demo import java.util.ArrayList; public class Demo03 { public static void main(String[] args) { ArrayList<BWM> bwms = new ArrayList<BWM>(); bwms.add(new BWM()); bwms.add(new BWM()); bwms.add(new BWM()); run(bwms); ArrayList<BENCHI> bcs = new ArrayList<BENCHI>(); bcs.add(new BENCHI()); bcs.add(new BENCHI()); bcs.add(new BENCHI()); run(bcs); } // 如果这里ArrayList使用泛型BWM,那么就只能BWM品牌的车子进行比赛 // public static void run(ArrayList<BWM> car) {} // 这里可以泛型的通配符 ? 就可以接收一切数据类型的ArrayList public static void run(ArrayList<?> car) {} } class Car{ } class BWM extends Car{ } class BENCHI extends Car{ }
-
-
泛型的上下限
? extend Car
: 那么?必须是Car 或者是其子类。? super Car
: 那么? 必须是Car 或者是其父类
-
二、高级教程
1、数据结构
- 队列
- 先进先出,后进后出
- 栈
- 进栈/压栈 出栈/弹栈 ,先进后出,后进先出原则
- 数组
- 链表
- 红黑树
- 二叉树
- 二叉平衡树
- 红黑树
2、Collection单列集合
-
集合的特点:1、类型可以不确定 2、大小不固定 3、集合有很多种,不同场景使用的集合不同
-
集合适用于开发中的增删改查操作、
-
Collection集合是java中集合的祖宗类
-
Collection的方法子类都可以使用 (集合重写了toString方法)
-
add()
: 添加元素,添加成功返回true。 -
clear()
:清空集合的所有元素。 -
isEmpty()
:判断集合是否为空。 -
size()
:获取集合的大小。 -
contains()
:判断集合中是否包含某个元素。 -
remove()
:删除某个元素,如果有多个重复的元素,默认删除最前面的一个。 -
toArray()
:将集合转为数组 ,接收的变量类型必须为Object[]
,如果要转成String类型的数组,就需要使用
toArray(String[]::new)
这样转出来的就是字符串数组。 -
addAll()
:可以将两个集合合并。
List集合的方法
add(index, element)
: 根据索引向集合中添加元素remove(index)
:根据索引删除元素get(index)
:根据索引获取元素set(index, element)
:用指定元素替换集合中指定位置的元素,返回更新前的元素
Collection集合的子类
-
Set系列集合:添加的元素是无序的、不重复的、无索引的。(Set系列集合是基于哈希表存储数据的,它的增删改查的性能)
-
—— HashSet: 添加的元素是无序的、不重复的、无索引的。
- —— LinkedHashSet:添加的元素是有序的、不重复的、无索引的。
-
—— TreeSet:按照大小排列,默认升序排序,不重复的、无索引的。(排序不重复集合 )
TreeSet集合自排序的方式
- 有值特性的元素直接可以按照升序排序
- 字符串类型的元素会按照首字符的ascii 排序
- 对于自定义的引用数据类型,TreeSet默认无法排序,执行的时候直接报错,因为人家不知道排序规则
自定义的引用数据类型的排序实现
对于自定义的引用数据类型,TreeSet默认无法排序
所以我们需要制定排序大小的规则,程序员制定排序大小规则的方案有2种:
-
直接为对象的类重写比较器规则接口Comparable,重写比较方法(拓展方式)
package javaSE.高级教程.Set集合.TreeSet集合; import java.util.Objects; public class Student implements Comparable<Student> { private String name; private int age; private String gander; public Student(String name, int age, String gander) { this.name = name; this.age = age; this.gander = gander; } // 重写实现 TreeSet的排序规则 @Override public int compareTo(Student 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; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + ", gander='" + gander + '\'' + '}'; } …… 省略getter and setter }
package javaSE.高级教程.Set集合.TreeSet集合; import java.util.TreeSet; public class demo { public static void main(String[] args) { TreeSet<Student> objects = new TreeSet<>(); objects.add(new Student("小潘", 20,"女")); objects.add(new Student("虎子", 25,"男")); objects.add(new Student("阮阮", 22,"女")); System.out.println("objects = " + objects); // 【结果】:objects = [Student{name='小潘', age=20, gander='女'}, // Student{name='阮阮', age=22, gander='女'}, // Student{name='虎子', age=25, gander='男'}] } }
2、直接为集合设置比较器Comparator对象,重写比较方法
// 若类和集合都有比较规则,就近原则,使用集合的比较规则 TreeSet<Student> objects1 = new TreeSet<>(new Comparator<Student>() { @Override public int compare(Student o1, Student o2) { return o2.getAge() - o1.getAge(); } }); objects1.add(new Student("小潘", 20,"女")); objects1.add(new Student("虎子", 25,"男")); objects1.add(new Student("阮阮", 22,"女")); System.out.println("objects1 = " + objects1); // objects1 = [Student{name='虎子', age=25, gander='男'}, // Student{name='阮阮', age=22, gander='女'}, // Student{name='小潘', age=20, gander='女'}]
-
-
Set系列集合无序的原因?
-
Set系列集合添加元素无序的根本原因是因为底层采用了哈希表存储元素
-
哈希表
-
JDK1.8之前:哈希表 = 数组 + 链表 + (哈希算法)
-
JDK1.8之后:哈希表 = 数组 + 链表 + 红黑树 + (哈希算法)
当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找的时间
-
-
哈希算法
- 先获取元素对象的哈希值
- 让当前对象的哈希值对底层数组长度求余
- 将求余的结果作为对象元素在底层数组的索引位置
- 把该对象元素存入到该索引位置,若该索引已有元素,就在此索引位置建立链表存储,(在)
-
-
Set系列集合不重复的原因?
-
Set系列集合会先让对象调用
hashCode()
方法获取两个对象的哈希值比较 / \
false true
/ \
不重复 继续让两个对象进行
equals
比较 / \
false true
/ \
不重复 重复了
需求:若只要对象的内容一样就认为两对象重复了,那么就需要重写对象的
hashCode
方法和equals
方法
-
-
List系列集合:添加的元素是有序的、可重复的、有索引的。(查询多增删少,使用ArrayList集合;查询少增删首位较多,用LinkedList集合)
- —— ArrayList:添加的元素是有序的、可重复的、有索引的。
- —— LinekdList:添加的元素是有序的、可重复的、有索引的。
addFirst(element)
:将指定元素插入此列表的开头。addLast(element)
:将指定元素插入此列表的末尾。getFirst()
:返回此列表的第一个元素getLast()
:返回此列表的最后一个元素removeFirst()
:移除并返回此列表的第一个元素removeLast()
:移除并返回此列表的最后一个元素pop()
:从此列表所表示的堆栈处弹出一个元素push(element)
:将元素推入此列表,所表示的堆栈
- —— Vector:是线程安全的,速度慢,工作中很少使用
Collection集合的遍历方式
一、迭代器
- 获取对应集合的迭代器对象
Iterator<String> it = 集合.iterator();
- 迭代器的基本方法
public Iterator iterator(){}
获取对应集合的迭代器对象next()
获取下一个元素Boolean hasNext()
判断是否有下一个元素
二、foreach(增强for循环)
-
优点:foreach遍历集合或数组很方便
-
缺点:foreach遍历无法知道遍历到哪个元素了,以为没有索引
-
foreach是一种遍历形式,可以遍历集合或者数组
-
foreach遍历集合实际上是迭代器遍历的简化写法
-
foreach遍历的关键是其语法格式
for(遍历集合的类型 变量:集合){}
三、lambda表达式,(jdk1.8之后的新技术)
集合.foreach(变量 -> {})
package javaSE.高级教程.迭代器; import java.util.ArrayList; import java.util.Iterator; public class demo { public static void main(String[] args) { ArrayList<String> lists = new ArrayList<>(); lists.add("贾乃亮"); lists.add("王宝强"); lists.add("小妹妹"); // new 出迭代器对象 Iterator<String> it = lists.iterator(); // public Iterator iterator(){} 获取对应集合的迭代器对象 // Boolean hasNext() 判断是否有下一个元素 // next() 获取下一个元素 // 1、迭代器遍历 遍历出lists的元素 while(it.hasNext()){ String ele = it.next(); System.out.println("ele = " + ele); } // 2、lambda表达式遍历 lambda表达式,jkd1.8之后的新技术 lists.forEach(s -> System.out.println("s = " + s)); // System.out.println("lists = " + lists); } }
-
3、Map双列集合
3.1 Map集合的体系
Map< K ,V >(接口,Map集合的祖宗类)
/ \
TreeMap< K ,V > HashMap< K ,V >(实现类、经典的,用的最多)
\
LinkedHashMap< K ,V >(实现类)
3.2 Map集合的特点
- Map集合的特点都是由键决定的
- Map集合的键是无序的,不重复的、无索引的,且Map集合后面重复的键会覆盖前面的整个元素
- Map集合的值无要求
- Map集合的键值对都可以为
null
- Map集合的键和值都可以为自定义类型,若需要判断自定义类型的键重复,那么就需要重写
hashCode()
和equals()
方法
3.3 Map集合的API
put(k,v)
向map集合中添加元素clear()
清除map集合中的所有元素isEmpty()
判断map集合是否为空, 返回布尔值get(key)
根据key获取valueremove()
根据key删除元素containsKey(key)
判断map集合是否包含key 返回布尔值containsValue(value)
判断集合是否包含某个值 返回布尔值keySet()
获取全部键的集合 返回值为Set()集合value()
获取全部值的集合 返回值为Collection集合size()
集合的大小putAll( Map )
合并两个map集合
3.4 Map集合的遍历
1、先使用keyset返回键的set集合,再通过遍历set集合获取value
2、键值对遍历方式,使用entrySet()
将map集合的键值对整体转化为set集合
Set<Map.Entry<String, Integer>> entries = maps.entrySet();
for (Map.Entry<String, Integer> entry : entries) {
String key = entry.getKey();
Integer value1 = entry.getValue();
System.out.println(key+"="+value1);
}
3、lambda表达式 forEach()
maps.forEach((k,v) -> System.out.println(k+"=="+v) );
3.5 LinkedHashMap
特点: 有序,不重复,无索引 的键值对集合
小结:1、HashMap是无序不重复的键值对集合
2、LinkedHashMap是有序不重复的键值对集合
3、他们都是基于哈希表存储数据的
3.6 TreeMap
1、双精度浮点型之间的比较,可以使用API Double.compare(o1,o2)
4、异常
4.1 什么是异常?
-
异常是程序在编译、执行的过程中可能出现的问题;
-
异常是应该尽量提前避免的
-
异常一旦出现,如果没有提前避免,就会导致程序退出JVM虚拟机而终止。
-
重写方法申明抛出的异常,应该与父类被重写方法的异常一致,或者更小
-
当处理多异常时,捕获处理,前面的异常类不能位后面异常类的父类
例如:try{} catch(Exception e){} catch(ParseException e){}
在这里Exception
类为ParseException
类的父类,所有后面抛出的ParseException
无意义
4.2 异常体系
Java中异常继承的根类是Thorwable
Throwable(根类,不是异常类)
/ \
Error Exception(异常,需要处理)
/ \
编译时异常 运行时异常(RuntimeException)
Error:错误的意思,严重错误Error,无法通过处理错误来解决,一旦出现,只能优化项目,再重启
Exception: 异常类, 它是在开发中代码在编译、执行期间可能出现的错误,它是需要提前处理的
4.3 Exception异常分类
1、编译时异常:继承Exception异常或者其子类,编译阶段就会报错,必须由程序员处理,否则代码编译不通过,无法执行
2、运行时异常:继承RuntionException的异常或则其子类,编译阶段是不会出错的,他是在运行阶段可能出现的错误,运行时异常可 以处理,也可以不处理,在编译期是不会报错的,但是在运行期间可能会报错,建议还是需要处理运行时可能出现的异常的
4.4 运行时异常(常见的)
-
数组索引越界:ArrayIndexOutOfBoundsException
int[] str = {1,2,3}; System.out.println(str[3]); // 【结果】Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
-
空指针异常:NullPointerException
int [] c = null; System.out.println(c.length); // 【结果】Exception in thread "main" java.lang.NullPointerException
-
类型转换异常:ClassCastException
// 例如:将String强转位Int类型 Object o = "齐天大圣"; Integer i = (Integer) o; // 【结果】Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
例如:将String强转位Int类型
-
迭代器遍历没有此元素异常:NoSuchElementException
Set<Object> set = new HashSet<>(); Iterator<Object> iterator = set.iterator(); System.out.println(iterator.next()); // 【结果】Exception in thread "main" java.util.NoSuchElementException
-
数学操作异常:ArithmeticException
int a = 10/0; // 【结果】Exception in thread "main" java.lang.ArithmeticException: / by zero
-
数字转换异常:NumberFormatException
String string = "21a"; Integer string_to_int = Integer.valueOf(string); System.out.println(string_to_int); // 【结果】Exception in thread "main" java.lang.NumberFormatException: For input string: "21a"
4.5 默认异常的处理机制
出现异常,系统默认的处理机制:默认会在出现异常的代码那里自动创建一个异常对象,在异常出现的地方,将这个异常对象抛出给调用者,调用者最终将异常抛给JVM虚拟机,虚拟机接收到异常对象之后,现在控制台输出异常栈信息,然后终止程序,后面的代码不执行。
4.6 编译时处理异常机制
1、使用throws Exception
向外抛出异常,缺点:麻烦,需要一层层的抛出异常
// main 抛出Exception
public static void main(String[] args) throws Exception {
parseTime("2013-12-10 10:19:23");
}
// parseTime的parse()可能出现异常,就需要处理这个异常,将其Exception抛出给上一层(mian函数)
public static void parseTime(String str) throws Exception {
SimpleDateFormat format = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
Date d = format.parse(str);
}
2、使用try{} catch(Exception e){}
抛出异常
public static void parseTime(String str) {
try{
SimpleDateFormat format = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
Date d = format.parse(str);
System.out.println(d);
} catch (Exception e) {
//输出异常日志
e.printStackTrace();
}
}
3、throws
与try{} catch(){}
结合使用抛出异常
在出现异常的地方,将异常一层一层抛出给最外层的调用者,最外层调用者集中捕获处理(异常处理的规范做法)
public static void main(String[] args) {
System.out.println("程序执行开始");
//在这里集中对异常进行处理,程序不会被终止
try {
parseTime("2013-12-10 10:19:23");
System.out.println("功能执行成功!");
} catch (Exception e) {
e.printStackTrace();
System.out.println("功能执行失败");
}
System.out.println("程序执行结束!");
}
//先将异常抛出给最外层的调用者
public static void parseTime(String str) throws Exception {
SimpleDateFormat format = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
Date d = format.parse(str);
System.out.println(d);
FileInputStream fileInputStream = new FileInputStream("D:/aaa.png");
}
4.7 运行时异常处理机制
运行时异常必须要进行处理,运行时异常会自动抛出,只需要在程序的最外层进行try() catch{}
抛出即可。
public static void main(String[] args) {
// 在这里可能会出现数学异常 所有使用try{} catch(){}
try{
chu(1,0);
}catch (Exception e) {
e.printStackTrace();
}
System.out.println("程序正常结束!");
}
// 运行时的异常会自动抛出,不需要手动抛出异常
public static void chu(int num1, int num2){
System.out.println(num1/num2);
}
4.8 关键字:finally
finally的作用:可以在代码执行完毕之后,进行资源释放操作。
在处理异常时的作用:try() catch{} finally{}
finally里面的代码,无论是否出现异常,最终一定会执行这里面的代码
4.9 自定义异常
- 先给自定义编译时异常类继承
Exception
类,再重写构造器 - 自定义运行时异常类,需要继承
RuntimeException
,再重写构造器 - throws 用于方法上抛出异常对象
- 运行时异常
throws RuntimeException
默认是抛出的,不需要写 - throw 用于创建异常对象,并从此处立刻抛出
package javaSE.高级教程.异常.自定义异常;
public class AgeIsIllegalException extends Exception {
// 1、自定义异常类继承Exception
// 2、重写构造器
public AgeIsIllegalException() {
}
public AgeIsIllegalException(String message) {
super(message);
}
public AgeIsIllegalException(String message, Throwable cause) {
super(message, cause);
}
public AgeIsIllegalException(Throwable cause) {
super(cause);
}
public AgeIsIllegalException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
package javaSE.高级教程.异常.自定义异常;
public class TestException {
public static void main(String[] args) {
try {
checkAge(10);
} catch (AgeIsIllegalException e) {
e.printStackTrace();
}
}
public static void checkAge(int age) throws AgeIsIllegalException {
if(age >200 || age <0) {
// throws 用于方法上抛出异常对象
// throw 用于创建异常对象,并从此处立刻抛出
throw new AgeIsIllegalException("/ this age is illegal");
}
}
【结果】
javaSE.高级教程.异常.自定义异常.AgeIsIllegalException: / this age is illegal
at javaSE.高级教程.异常.自定义异常.TestException.checkAge(TestException.java:16)
at javaSE.高级教程.异常.自定义异常.TestException.main(TestException.java:6)
}
5、多线程编程 JUC
5.1 认识线程
什么是进程?
- 程序是静止的,运行中的程序就是一个进程
- 进程具有三特性
- 动态性:进程是运行中的程序,需要动态占用cpu、内存、和网络等资源
- 独立性:每个进程都是相互独立的,彼此有自己的独立内存区域
- 并发性:假如cpu是单核的,同一个时刻其实内存中只有一个进程在被执行,CPU会分时轮询切换为每个进程进行服务,切换的速度非常快,以至于我们的感觉多个进程是同时执行的什么是线程?
什么是线程?
- 线程是属于进程的,一个进程可以有多个线程,这就是多线程
- 线程的创建相对于进程来说,开销较小
- 线程也支持并发
5.2 线程的常用API
setName(String name)
设置线程的名称 ,注意:要在线程开启之前设置getName()
获取当前线程的名称public static Thread currentThread()
获取当前线程对象,在哪个线程里就获取哪个线程对象public static void sleep(long time)
让当前线程休眠多少毫秒,再继续执行
sleep()
与wait()
有什么区别?
sleep设置线程等待的时间,线程苏醒的时间是sleep这时候就设置好的,而wait()是让线程进入等待状态,其线程的苏醒时间可以由程序员精确控制的
5.3 线程的创建
5.3.1 ①_继承方式
如何创建一个线程?
继承Thread
类创建线程:缺点 - > 线程类已经继承了Thread父类,无法继承其他类,功能无法拓展 优点: 编码简单
-
创建一个线程类并继承
Thread
父类 -
在重写父类的
Run()
方法,此时只是创建好了线程类public class MyThread extends Thread{ //创建一个线程类继承Thread @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("子线程输出111:"+i); } } }
-
在main方法创建线程类对象,并调用线程对象的
start()
方法,开启一个线程,main方法也是一个线程Thread th = new MyThread(); th.start();
线程的注意事项
- 线程启动必须调用
start()
方法,否则当成普通类处理(例如:调用run()
启动线程,错误做法)- —
start()
方法底层其实是给CPU注册当前线程,并且触发run()
方法执行
- —
- 建议先创建子线程,主线程的任务放在之后,否则主线程任务永远先执行完毕,再执行子线程任务
5.3.2 ②_实现Runnable接口
实现Runnable
接口创建线程的优点:
– 线程任务类只是实现了Runnable接口,可以继承其他类,而且可以继续实现其他接口
– 同一个线程任务对象,可以被包装为多个线程对象
– 适合多个线程去共享同一个资源
– 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码与线程独立
– 线程池可以放入实现Runnable或Callable线程任务对象
注意:其实Thread类也实现了Runnable接口的
缺点:不能直接获取到线程执行的结果
-
先创建线程任务类实现
Runnable
接口class RunableThread implements Runnable{}
-
重新
Run()
方法@Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName()+"==>"+i); try { Thread.sleep(550); } catch (InterruptedException e) { e.printStackTrace(); } } }
-
创建线程任务对象
Runnable r = new RunableThread();
-
利用Thread的构造器,将Runnable包装成Thread线程对象,并开启线程
Thread t = new Thread(r,"1号线程"); t.start();
5.3.3 ③_实现Callable接口
优点:
– 线程任务类只是实现了Runnable接口,可以继承其他类,而且可以继续实现其他接口
– 同一个线程任务对象,可以被包装为多个线程对象
– 适合多个线程去共享同一个资源
– 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码与线程独立
– 线程池可以放入实现Runnable或Callable线程任务对象
缺点:
– 编码复杂
-
创建线程任类实现接口Callable
class MyCallable implements Callable<V>
-
重写
call()
方法@Override public String call() throws Exception { int sum =0; for (int i = 0; i < 11; i++) { System.out.println(Thread.currentThread().getName()+"=>"+i); sum+=i; } return Thread.currentThread().getName()+"线程执行结果为"+sum; }
-
创建Callable线程任务对象
Callable callable = new MyCallable();
-
将callable包装成一个未来任务对象
FutureTask<String> task = new FutureTask<>(callable);
-
将task包装成线程任务对象
未来任务对象是什么?
未来任务对象其实就是一个Runnable对象,这样就可以被Thread包装成线程任务对象
未来任务对象可以在线程执行完毕后使用get()
方法获取结果Thread t = new Thread(task);
-
启动线程
t.start()
-
获取线程的执行结果
try { String result = task.get() System.out.println(result); } catch (Exception e) { e.printStackTrace(); }
5.4 线程安全
5.5 线程同步
线程同步解决线程安全问题的核心思想:让多个线程实现先后依次访问共享资源。
线程同步的方式有:
- 同步代码块
- 同步方法
- lock锁
5.5.1 同步代码块
作用:把出现线程安全问题的核心代码块上锁,每次仅允许一个线程进入,执行完毕之后自动解锁,其他线程才可以进来执行。
语法:synchronized (锁对象) {核心代码}
锁对象:理论上可以是任意的唯一对象即可。
原则上:锁对象建议使用共享资源
– 在实例对象方法中建议使用this
作为锁对象。
– 在静态方法中建议用类名.class
字节码作为锁对象(字节码名是唯一的)
性能:锁住的代码越少,性能越好
public void draw(double money) {
String name = Thread.currentThread().getName();
synchronized (this) {
if(this.money >= money){
System.out.println(name+"来取钱,余额充足,吐出"+money);
this.money-=money;
System.out.println(name+"来取钱后,余额剩余:"+this.money);
} else {
System.out.println(name+"来取钱,余额不足!");
}
}
}
5.5.2 同步方法
作用:把出现线程安全问题的核心方法给锁起来
用法:直接给方法加上一个修饰符synchronized
原理:同步方法的原理其实和同步代码块的底层原理是完全一样的,同步方法只不过是将整个方法的代码都锁起来
同步方法的底层其实也是有锁对象的:
如果方法是实例方法:同步方法默认使用this作为锁对象
如果方法是静态方法:同步方法默认使用类名.class
作为锁对象
// 给方法添加修饰符 synchronized 即可
public synchronized void draw(double money) {
String name = Thread.currentThread().getName();
if(this.money >= money){
System.out.println(name+"来取钱,余额充足,吐出"+money);
this.money-=money;
System.out.println(name+"来取钱后,余额剩余:"+this.money);
} else {
System.out.println(name+"来取钱,余额不足!");
}
}
5.5.3 lock锁
Lock锁也成为同步锁,加锁和是否锁方法化,如下:
- `public void lock()`: 加同步锁。
- `public void unlock()`: 释放加同步锁。
-
创建一个锁对象:在对象类中创建锁的实例对象,这样相对于每个对象来说都是唯一的
private Lock lock = new ReentrantLock();
-
开启锁
lock.lock();
-
释放锁
lock.unlock();
注意:
为了防止锁住的代码块发生异常,不执行解锁操作,造成死锁,我们需要将释放锁的操作放在
finally
中进行解锁,锁住的代码块也应该放在try{}catch(){}
// 开启锁 lock.lock(); try{ if(this.money >= money){ System.out.println(name+"来取钱,余额充足,吐出"+money); this.money-=money; System.out.println(name+"来取钱后,余额剩余:"+this.money); } else { System.out.println(name+"来取钱,余额不足!"); } }catch (Exception e) { e.printStackTrace(); }finally { // 释放锁 lock.unlock(); }
5.6 线程通信
线程通信:多个线程以为在同一个进程中,所以相互通信比较容易
注意:线程通信一定是多个线程在操纵同一个资源才需要进行通信
线程通信的核心方法:
public void wait()
: 让当前线程进入到等待状态,此方法必须由锁对象调用。public void notify()
:唤醒当前锁对象上等待状态的某个线程,此方法必须由锁对象调用。public void notifyAll()
唤醒当前锁对象上等待状态的全部线程,此方法必须由锁对象调用。
5.7 线程池
5.7.1 线程池的概念
线程池:其实就是一个可以容纳多个线程的容器,其中的线程可以反复利用,省去了重复创建线程对象的操作,不需要反复创建线程而造成过多的资源消耗。
线程池的优点:
- 降低资源消耗
- 减少创建线程和销毁线程的次数,每个线程都可以被重复利用,可执行多个任务。
- 提高响应速度
- 不需要频繁的创建线程,如果有线程可以直接用,不会出现系统死掉。
- 提高线程的可管理性
- 线程池可以约束系统最多只能有多少个线程,不会造成因为系统线程过多而死机。
5.7.2 线程池的使用
1、创建线程池
通过Executors.class
提供的public ExecutorService newFixedThreadPool(int nThread){}
// 创建一个线程池, 线程最大数为3
ExecutorService pools = Executors.newFixedThreadPool(3)
2、往线程池中添加任务
ExecutorService提交线程任务的方法public Future<?> submit(Runnable task)
ExecutorService提交线程任务的方法public Future<?> submit(Callable task)
// 2、往线程池中添加任务
// 像线程池中提交Runnable线程任务
MyRunnable myRunnable = new MyRunnable();
pools.submit(myRunnable);
// 向线程池中提交Callable线程任务
MyCallable myCallable = new MyCallable();
Future<String> t1 = pools.submit(myCallable);
// 获取myCallable线程任务的执行结果
String result = t1.get()
3、关闭线程池
shutdown()
:等待线程完成任务后关闭线程池shutdownNow()
: 立即关闭线程池,不论线程任务是否执行完毕
5.8 死锁
java死锁产生的四个必要条件:
1、互斥使用
2、不可抢占
3、请求与保持
4、循环等待
5.9 Volatile关键字
5.9.1 并发变量下变量不可见性问题
引入:在多个线程访问共享变量过程中, 一个线程修改变量的值后,其他线程看不到变量最新值的情况。
1、VolatileDemo01线程在run方法中修改flag变量的值为true
@Override
public void run() {
flag = true;
System.out.println(Thread.currentThread().getName()+":"+"flag = " + flag);
}
2、在Test类中启动VolatileDemo01线程并且在Test主线程中监视flag变量
while (true){
if(t.isFlag()){
System.out.println("主线程进入执行");
}
}
3、运行结果:VolatileDemo01线程将flag修改之后,Test主线程并没有监控到flag有变化,这就是并发变量下变量不可见性问题
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
完整代码如下:
public class VolatileDemo01 extends Thread{
// private volatile boolean flag;
private boolean flag;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
flag = true;
System.out.println(Thread.currentThread().getName()+":"+"flag = " + flag);
}
public VolatileDemo01(boolean flag, String name) {
super(name);
this.flag = flag;
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
class Test {
public static void main(String[] args) {
// 启动子线程
VolatileDemo01 t = new VolatileDemo01(false, "子线程");
t.start();
// 主线程
while (true){
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+"flag = "+t.isFlag());
// 这里需要使用到VolatileDemo01中的变量
synchronized (VolatileDemo01.class){
if(t.isFlag()){
System.out.println("-------------------------");
System.out.println("主线程进入执行");
System.out.println(Thread.currentThread().getName()+":"+"flag = "+t.isFlag());
System.out.println("-------------------------");
}
}
}
}
}
5.9.2 变量不可见性的内存语义
在介绍多线程并发修改变量不可见现象的问题原因之前,我们需要了解一下Java内存模型(java并发编程有关的模型):JMM
JMM(Java Memory Model);Java内存模型,是Java虚拟机规范中所定义的一种内存,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别。
Java内存模型描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节
JMM有一下规定:
- 所有的共享变量都存储于主内存,这里所说 的变量指的是实例变量和类变量,不包括局部变量,因为局部变量是线程私有的,不存在竞争问题。
- 每一个线程还存在自己的工作内存,线程的工作内存保留了被线程使用的变量的工作副本
- 线程对变量的读写操作都是在工作内存中完成,而不是直接读写与主内存中的变量。
- 不同线程之间不能直接访问对方工作内存中的变量,线程之间的变量传递需要通过主内存中转来完成。
5.9.3 解决方案(并发变量下变量不可见性问题)
① synchronized
加锁实现重新将主内存的共享变量 拷贝至子线程的工作内存。
while (true){
synchronized (VolatileDemo01.class){
if(t.isFlag()){
System.out.println("主线程进入执行");
}
}
}
② 给共享变量加上volatile关键字
private volatile boolean flag;
5.9.4 volatile
与synchronized
volatile
只能修饰实例变量和类变量,而synchronized
可以修饰方法以及代码块volatile
保证数据的可见性问题,但是不保证原子性(多线程进行缬草组,不保证线程安全)而synchronized
是一种排他(互斥)的机制。即volatile
不保证线程安全,而synchronized
是线程安全的
5.9.5 volitile 关键字
使用volatile
修饰的变量的值在被一个线程修后,其他线程可以获取到这个共享变量的最新值
使用volatile修饰共享变量,就可以解决并发变量下变量不可见性问题
5.10 原子性
5.10.1 volatile修饰变量的原子性研究
虽然volatile关键字解决了并发变量下变量不可见性问题,但是新的问题来了。
线程任务的原子性问题:例如一笔交易,买方付款这时买方的账户会-100元,此时卖方的账户会+100元,这两个操作是缺一不可的,但是在多线程中可能其中一方的操作失败,就会出现逻辑错误。这就是多线程的原子性问题
实验:创建100个线程,每个线程任务是count++
这样循环100次。
大家可能觉得,结果就是100*100 = 10000
但是结果并不如大家所愿,并不是每次运算出的结果都是10000,可能会小于10000
这里就是多线程非原子性
public class VolatileAtomicDemo01 {
public static void main(String[] args) {
Runnable target = new MyRunnable();
for (int i = 0; i < 100; i++) {
// 启动100个线程
new Thread(target).start();
}
}
}
class MyRunnable implements Runnable{
private volatile int count;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
count++;
System.out.println("count = " + count);
}
}
}
为保证原子性操作的方法:
1、加锁synchronized
:但是用加锁的方式保证原子性操作的性能低下,因为synchronized
锁住的是一块代码块,这样子是效率是不高的,这个种方法通常被成为悲观锁
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (this){
count++;
System.out.println("count = " + count);
}
}
}
2、使用原子类来保证原子性操作:
Java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包)引入原子类AtomicInteger来保证原子性操作的方式,性能高效,线程安全,一般被称为乐观锁
原子类Integer型,可以实现原子操作
public AtomicInteger(); 初始化一个默认值为0的原子型Integer。
public AtomicInteger(int initialValue); 初始化一个指定值的原子型Integer。
public int get(); 获取值。
public int getAndIncrement(); 以原子方式将当前值加1,并返回**自增前**的值。
public int incrementAndGet(); 以原子方式将当前值加1,并返回**自增后**的值。
public int addAndGet(int data); 以原子方式将输入的数值与实例中的值相加,并返回结果。
public int getAndSet(int value); 以原子方式设置为newValue的值,并返回旧值。
接下来我们用代码来实现Atomic包实现原子性操作
// 创建一个AtomicInteger型的共享变量
private AtomicInteger atomicInteger = new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 使用AtomicInteger包保证原子性操作
System.out.println("count ===> " + atomicInteger.incrementAndGet());
}
}
5.11并发包
5.11.1 ConcurrentHashMap
① HashMap
之前我们学习过HashMap集合,但是在多线程中,HashMap不是线程安全的,而在多线程中使用HashMap集合就要使用线程安全的并发包ConcurrentHashMap
② HashTable
在ConcurrentHashMap之前,还有一种线程安全的HashMap集合叫做HashTable集合,虽然HashTable解决了线程安全问题,但是其效率是低下的。
原因HashTable底层实现线程安全的方法是使用synchronized修饰方法,从而达到线程安全,这样子同一时间只能对集合进行一个操作,例如,如果真正该获取该map集合的值,那么就不能同时向map集合中添加,或删除等操作。
③ ConcurrentHashMap
ConcurrentHashMap解决了HashTable性能低下的问题,HashTable的锁是锁住整个对象,而ConcurrentHashMap是采用CAS(compare and swap)机制和局部锁(synchronized)分段式锁。
API:
- 没啥不一样的,和HashMap 的API差不多
5.11.2 CountDownLatch
CountDownLatch和线程的休眠wait方法有些类似 但是也有些区别,线程被wait等待之后,只需要notify()
或者notifyAll()
唤醒一次就可以(notify()
几率唤醒,但是,唤醒某个线程,就直接被唤醒了,只需要一次)。CountDownLatch唤醒的机制和类似。
API:
public CountDownLatch(int count)
初始化唤醒某个线程需要几步public void await()
让当前线程等待,必须countDown到0,才会被唤醒public void countDown()
每次调用一次count -1
6、通信
7、图片上传
8、注解
9、反射
- 反射是java独有的技术
- 反射是只对于任何一个类
11、发送邮件
-
使用QQ邮箱进入设置 => 账号 => 开启SMTP服务
-
获取授权码 bkzb llqx yael difd
-
这里使用spring boot框架 使用依赖有: java web , java mail server, thymeleaf模板
-
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
-
配置邮箱
# 邮箱配置 # qq邮箱的服务器主机名 spring.mail.host=smtp.qq.com #端口 spring.mail.port=25 #编码方式 spring.mail.default-encoding=UTF-8 # username 就是发件人的邮箱 spring.mail.username=1620460256@qq.com # password 就为上面获取到的邮箱授权码 spring.mail.password=bkzbllqxyaeldifd #---------------------------------------------------------------------------- #模板邮箱配置 spring.thymeleaf.cache=true # 检查模板是否存在,然后再呈现 spring.thymeleaf.check-template=true # 检查模板位置是否正确(默认值 :true ) spring.thymeleaf.check-template-location=true #Content-Type 的值(默认值: textml ) spring.thymeleaf.content-type=text/html # 开启 MVC Thymeleaf 视图解析(默认值: true ) spring.thymeleaf.enabled=true # 模板编码 spring.thymeleaf.encoding=UTF-8 # 要被排除在解析之外的视图名称列表,?逗号分隔 spring.thymeleaf.excluded-view-names= # 要运?于模板之上的模板模式。另? StandardTemplate-ModeHandlers( 默认值: HTML5) spring.thymeleaf.mode=HTML5 # 在构建 URL 时添加到视图名称前的前缀(默认值: classpath:/templates/ ) spring.thymeleaf.prefix=classpath:/templates/ # 在构建 URL 时添加到视图名称后的后缀(默认值: .html ) spring.thymeleaf.suffix=.html
-
创建一个工具类(发送邮箱的函数)
// SendMail.class @Service public class SendMail { @Resource JavaMailSender javaMailSender; @Resource ThyUtils thyUtils; @Value("${spring.mail.username}") private String username; //发送一封简单的纯文本邮件 public int testSendMail(){ SimpleMailMessage simpleMailMessage = new SimpleMailMessage(); // 收件人的邮箱 simpleMailMessage.setTo("3284977179@qq.com"); // 发件人的邮箱 simpleMailMessage.setFrom(username); // 邮件标题 simpleMailMessage.setSubject("这是邮件的标题"); // 邮件内容 simpleMailMessage.setText("这是一封测试邮件的主体内容<h1>一级标题</h1><a href='www.baidu.com'>百度连接</a>"); // 最后使用javaMailSender 这个类的send(simpleMailMessage对象) 方法,发送邮件...... javaMailSender.send(simpleMailMessage); return 1; } //发送一封模板邮件,就是可以自定义html页面的邮件 public int comflexMail(){ MimeMessage mimeMessage = javaMailSender.createMimeMessage(); try { MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage,true); mimeMessageHelper.setTo("3284977179@qq.com"); mimeMessageHelper.setFrom(username); mimeMessageHelper.setSubject("模板邮件"); // setText 参数1: 模板, 参数2:是否使用html编码方式解析模板 mimeMessageHelper.setText(thyUtils.getTemplate(),true); } catch (MessagingException e) { e.printStackTrace(); } // 发送邮件 javaMailSender.send(mimeMessage); return 1; } }
// ThyUtils.class @Component public class ThyUtils { @Resource TemplateEngine templateEngine; public String getTemplate(){ // 创建一个实体类,用来存储模板中的变量 Context context = new Context(); // 向这个实体类中添加 键值对 context.setVariable("name","李文杰"); context.setVariable("url","https://www.baidu.com"); // 这里的process()有两个参数: 1、html模板的文件名(不需要后缀.html) 2、向此模板中传入的变量,需要放在context实体类中 String result = templateEngine.process("template", context); // System.out.println("result:"+result); //返回的是将html页面转成字符串然后return出去 return result; } }
// html 模板 // 路径:src\main\resources\templates\template.html <!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>模板邮件</title> </head> <body> <h2 th:text="${name}"></h2><h2 style="color: #ff0000">!</h2> <h3>恭喜你!</h3> <p>成功通过</p> <a th:href="${url}">软件创新工作室2021秋的招新计划</a> </body> </html>
-
使用controller接口 触发这个发送邮件的函数
// TestController.class @RestController public class TestController { @Resource SendMail sendMail; @GetMapping("/test") public String mail(){ int result = sendMail.testSendMail(); if(result == 1) return "发送成功"; return "发送失败"; } @GetMapping("/complex") public String comflex(){ int result = sendMail.comflexMail(); if(result == 1) return "发送成功"; return "发送失败"; } }
-
三、工具类
1、API积累
1.1、字符串转数组
// 字符串.toCharArray()
char[] numArr = num.toCharArray();
1.2、整数转字符串
//String.valueOf(整数)
int num = 123;
String numStr = String.valueOf(num);
2、日期时间
- SimpleDateFormat 工具类
public int verInvitationCode(String invitationCodeParams) {
ApplyTopic applyTopic = applyTopicMapper.verInvitationCode(invitationCodeParams);
// 此验证码是否正确
if (applyTopic!=null){
// 验证码正确,校验验证码是否过期
// 格式化时间
SimpleDateFormat format = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
try {
// 开始时间
Date startTime = format.parse(format.format(applyTopic.getStartTime()));
// 结束时间
Date endTime = format.parse(format.format(applyTopic.getEndTime()));
// 现在的时间
Date now = format.parse(format.format(new Date()));
// 现在时间与开始时间比较
Boolean s = startTime.before(now);
// 现在时间与结束时间比较
Boolean e = now.before(endTime);
// 验证码未过期
if(s && e) return 1;
// 验证码过期
return 2;
} catch (ParseException e) {
e.printStackTrace();
}
}
// 邀请码错误
return 0;
}
Springboot Security
Json Web Token(JWT)
一、JWT说明
JWT由三部分组成
1、header 头部
{
"typ":"JWT", // token类型
"alg":"HS256" // 加密算法
}
2、payload 有效载荷
有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择。
- iss:发行人
- exp:到期时间
- sub:主题
- aud:用户
- nbf:在此之前不可用
- iat:发布时间
- jti:JWT ID用于标识该JWT
{
// 默认字段
"subject":"admin", // 主体
"expiration":"1000*24*60*60", //过期时间 一天
"id":"1001", // token的id
// 私有字段
"username":"lwj", // 用户名
}
3、signature 签名
签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。
首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名。
HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload),secret)
二、生成JWT
要是用JWT首先需要JWT 的依赖
1、JDK1.8只需要引入以下依赖就可以
<!-- jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2、生成token
private long time = 1000*24*60*60; // token有效期
private String signature = "admin"; // 密钥,不知道密钥是无法逆向破解token的
/**
* 生成token
* @param username
* @return
*/
public String makeToken(String username){
JwtBuilder builder = Jwts.builder();
String jwtToken = builder
// header jwt头部信息
.setHeaderParam("typ", "JWT") // token类型
.setHeaderParam( "alg","HS256") // 加密方式
//payload 有效载荷
.claim("username", username) // 私有字段 可以自定义
.setSubject("admin-test") // token主题 // 一下都是JWT提供的自选载荷
.setExpiration(new Date(System.currentTimeMillis()+time)) // 设置token过期时间
.setId(UUID.randomUUID().toString())
// Signature 签名
.signWith(SignatureAlgorithm.HS256, signature) // 声名签名算法,和上面header的加密方式一致,第二个参数为密钥
.compact(); // 将三个部分用“.”拼接起来
return jwtToken;
}
三、解密(解析token)
/**
* 解析token
* @param token
*/
public void parse(String token){
try{
JwtParser parser = Jwts.parser();
// 传入密钥 传入token
Jws<Claims> claimsJws = parser.setSigningKey(signature).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
System.out.println(claims);
System.out.println(claims.getId());
} catch (Exception e){
e.printStackTrace();
}
}
// 【结果】{username=liwenjie, sub=admin-test, exp=1642261518, jti=067695a0-7190-4162-9ada-47f84351819a}
// 067695a0-7190-4162-9ada-47f84351819a
Redis
nginx
可以用于实现负载均衡,将大量的请求平均分摊到每个服务器上,这样服务器就不会因为大量请求宕机,而其他服务器处理少量请求。
NoSQL
概念:redis是一种典型的Nosql数据库,Nosql数据库的存储是没有I/O操作的,是一种非关系型数据库
redis的数据支持持久化,并且是直接存储在内存中的,非常适用于高并发,提高性能。
为什么需要引入Nosql数据库,这里我们先解决一个多服务器上,用户登陆的时候,服务器需要保存用户的session信息,这时候只有用户登陆请求的那台服务器存储了用户的session信息,其他服务器没有此用户的登陆信息,这就会导致用户登录后,进行其他操作失败,原因就是,其他服务器没有该用户的session信息。
解决多服务器运行项目,session存储在哪里?
1、将session存储在客户端的Cookie里
- 这样不安全
- 网络负担效率低下
2、存储在文件服务器,或者数据库里
- 这样会带来大量的IO操作,效率问题会显现出来
3、session复制
- 这样显而易见,会消耗大量的存储空间,造成数据冗余
4、缓存数据库(NoSQL)
- 优点: 数据完全在内存中,速度快,数据结构简单(key-value)键值对形式
拦截器实现登录功能
使用拦截器拦截所有接口,(登录注册首页接口除外)
1、当用户登录的时候,生成token,并且将token存入redis,并设置过期时间
2、拦截器,后端接收请求
- 首先来到拦截器,获取用户的请求头中的token
- 然后在redis里面查询是否含有此token
- 如果有则放行,没有则拦截并返回·
用JWT首先需要JWT 的依赖
1、JDK1.8只需要引入以下依赖就可以
<!-- jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2、生成token
private long time = 1000*24*60*60; // token有效期
private String signature = "admin"; // 密钥,不知道密钥是无法逆向破解token的
/**
* 生成token
* @param username
* @return
*/
public String makeToken(String username){
JwtBuilder builder = Jwts.builder();
String jwtToken = builder
// header jwt头部信息
.setHeaderParam("typ", "JWT") // token类型
.setHeaderParam( "alg","HS256") // 加密方式
//payload 有效载荷
.claim("username", username) // 私有字段 可以自定义
.setSubject("admin-test") // token主题 // 一下都是JWT提供的自选载荷
.setExpiration(new Date(System.currentTimeMillis()+time)) // 设置token过期时间
.setId(UUID.randomUUID().toString())
// Signature 签名
.signWith(SignatureAlgorithm.HS256, signature) // 声名签名算法,和上面header的加密方式一致,第二个参数为密钥
.compact(); // 将三个部分用“.”拼接起来
return jwtToken;
}
三、解密(解析token)
/**
* 解析token
* @param token
*/
public void parse(String token){
try{
JwtParser parser = Jwts.parser();
// 传入密钥 传入token
Jws<Claims> claimsJws = parser.setSigningKey(signature).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
System.out.println(claims);
System.out.println(claims.getId());
} catch (Exception e){
e.printStackTrace();
}
}
// 【结果】{username=liwenjie, sub=admin-test, exp=1642261518, jti=067695a0-7190-4162-9ada-47f84351819a}
// 067695a0-7190-4162-9ada-47f84351819a
Redis
nginx
可以用于实现负载均衡,将大量的请求平均分摊到每个服务器上,这样服务器就不会因为大量请求宕机,而其他服务器处理少量请求。
NoSQL
概念:redis是一种典型的Nosql数据库,Nosql数据库的存储是没有I/O操作的,是一种非关系型数据库
redis的数据支持持久化,并且是直接存储在内存中的,非常适用于高并发,提高性能。
为什么需要引入Nosql数据库,这里我们先解决一个多服务器上,用户登陆的时候,服务器需要保存用户的session信息,这时候只有用户登陆请求的那台服务器存储了用户的session信息,其他服务器没有此用户的登陆信息,这就会导致用户登录后,进行其他操作失败,原因就是,其他服务器没有该用户的session信息。
解决多服务器运行项目,session存储在哪里?
1、将session存储在客户端的Cookie里
- 这样不安全
- 网络负担效率低下
2、存储在文件服务器,或者数据库里
- 这样会带来大量的IO操作,效率问题会显现出来
3、session复制
- 这样显而易见,会消耗大量的存储空间,造成数据冗余
4、缓存数据库(NoSQL)
- 优点: 数据完全在内存中,速度快,数据结构简单(key-value)键值对形式
拦截器实现登录功能
使用拦截器拦截所有接口,(登录注册首页接口除外)
1、当用户登录的时候,生成token,并且将token存入redis,并设置过期时间
2、拦截器,后端接收请求
- 首先来到拦截器,获取用户的请求头中的token
- 然后在redis里面查询是否含有此token
- 如果有则放行,没有则拦截并返回·