目录
Java类与对象学习学习路线
名词的别称
权限修饰符(访问控制权限)
属性默认值
类与对象定义
对象的定义和使用
成员属性的权限
构造方法
区别:深拷贝和浅拷贝
成员属性封装、构造重载实例
为何要封装?
引用传递(浅拷贝)与垃圾分析
匿名对象(不声明,直接实例化):
使用构造方法(构造器)接受引用类型
JavaBean是一种Java语言写成的可重用组件
this关键字
this调用构造器
package关键字和import关键字
继承性:
四种可见性测试
如何理解继承和权限的可见性?
继承与可见性的总结
public类和缺省类有什么区别?
继承性的一些规定
继承:子类的构造
this()和super()具体使用
super(形参列表)构造器
子类对象实例化的
方法覆写(重写):名字 形参相同,返回值大致相同
方法重载和重写(覆写)的区别:
覆写的意义
覆写后如何调用父类被覆写的方法?
方法覆写的限制条件
属性覆写:只要求名字相同
多态性
对象的多态性
方法的多态性:(调用谁的方法?)
instanceof方法
属性没有多态性:(子类赋给父类不会覆写属性)
属性不重定义,构造方法也可以决定属性值
子类不重定义属性
不在子类重新定义int age 则不构成多态,任何方法都能访问修改唯一的age值
对象多态性总结
虚拟方法调用VMI
虚拟方法调用(多态情况下):
包装类概念
包装类的继承
基本数据类型——>>包装类 的转换
默认值的区别
包装类——>>基本数据类型 的转换
xxValueof方法 toString方法 区别
自动装箱
自动拆箱
基本数据类型、包装类——>String类型 的转换
Sting类型——>基本数据类型、包装类 的转换
instanceof用法
toString用法
未重写toString的情况,返回的是地址信息
重写toString方法,返回实体信息
static关键字
static定义属性
static定义方法
final关键字
代码块
普通代码块{}
静态代码块static{ }
执行顺序
有继承的代码块
先父后子,静态先行
抽象类
abstract类的匿名子对象
类作为属性
接口
接口多态性,接口的匿名子类
1.有名对象,匿名子类
2.无名对象,匿名子类
JDK8之后的接口
类优先原则
内部类
定义
成员内部类
JVM的类加载规则 :
内部类的实例化
局部内部类的使用
为什么要异常处理?
异常处理
异常概述和异常体系结构
Java异常分为两大类
Error
Exception
异常体系结构
常用的三个异常处理语句
异常处理机制
抓抛模型
异常处理机制一:try-catch-finally
finally是可选的
异常处理机制二:throws + 异常类型
理解开发中的异常处理
继承中的异常方法重写
手动抛出异常对象 throw
自定义异常
程序、进程、线程
CPU的单核、多核
并发、并行
多线程的创建、使用
Thread类
常用方法
多线程创建方式一:继承于Thread类
给线程命名:
线程优先级
多线程创建方式二:实现Runnable接口
两种方式的对比
多线程创建方式三:有返回值:Callable接口
Callable接口的使用
多线程创建方式四:线程池
线程的生命周期
线程安全问题
线程同步synchronized
synchronized同步代码块
因此,同步代码块既不能框的太少,也不能框的太多
synchronized同步方法
死锁
公平锁、非公平锁
jdk5之后提供了ReentrantLock类
wait和notify、notifyAll
wait和sleep的区别
为什么同步代码块不能框多也不能框少
线程通信:生产者消费者源码与解析
以下的4个API仅作参考,JDK8之后用途很少
JDK8之后的时间相关API
LocalDate、LocalTime、LocalDateTime
注:我在写这篇博客的时间是2021-08-24和2021-08-25凌晨
静态的now方法:获取当前时间
静态的of方法:设置指定的时间
getXxx方法:获取年月日
withXxx方法:设置年月日时分秒
with和of方法的区别
plus、minus加减方法
总结LocalXxxXxx的三个类
Instant
Instant中:now、ofEpochMilli、toEpochMilli方法
Instant中:atOffset(ZoneOffset. ofHours(x))设置时区方法
DateTimeFormat格式化与解析日期时间
format和parse方法
不可变性
日期总结
1,类和类成员:属性(C++叫成员)、方法(C++叫函数)、构造器(构造方法)(C++叫构造函数);代码块、内部类
2,面向对象三大特征:封装性、继承性、多态性、(抽象性)
- 封装性:
①对外隐藏方法实现细节;隐藏属性,只能通过 类.访问
②有些属性,行为 只能用,不能改
- 继承性:
父类(超类) —继承—>> 子类(派生类):继承并可以添加自己的新成员
Java目前不支持多继承,但是通过 接口(interface)的方式弥补了一个子类不能继承多个父类的缺陷。
- 多态性:允许出现重名现象,Java中包括两种
①方法重载:同名方法形参的个数 类型不同,调用的方法不同
②对象多态:子类对象可以和父类对象相互转换,子类不同,则完成的功能不同
- 抽象性:(不属于三大特征):abstract
3,其他关键字:this、super、static、final、abstract、interface、package、import...
属性=成员变量=field=域=字段
方法=成员方法=函数=method
private、public、protected、default(默认,缺省,什么都不写)
一个类结构的本质就是 相关变量+方法
class Person{//大写
String name;//访问控制权限是缺省的
int age;//访问控制权限是缺省的
//定义两个属性,会自动给出默认值 null和0
public void tell(){ //不加static
//方法的定义和调用都加(),分为有参和无参两种情况
System.out.println( name + '\t' + age );
//在方法中可以直接访问成员变量(属性)
}
}
Q:为什么这个地方定义方法tell()没有加static?
A:调用方式不同,由对象调用的方法在定义时不用加static,如果不是则加static(例如在主类 public class Test{}中定义,并且由主类直接调用的方法必须加上static)
类 对象 = new 类();
对象 . 属性 = xxxxx ;
对象 . 成员函数();
public class Test{
public static void main(String [] args){
Person zhang = new Person();
//声明并实例化的时候要加(),因为Java需要判断用什么构造方法
/*
*还可以分两步来声明和实例化
*声明: Person zhang = null ;
*实例化:zhang = new Person();
*/
zhang.name = "张三";//类外调用对象的属性
zhang.age = 20 ;//类外调用对象的属性
zhang.tell();
//使用方法的时候要加(),因为Java需要判断有没有参数
}
}
面向对象的一大特点就是封装性,而封装性的一大特点就是内部结构对外不可见
上面代码例子中,String name;和int age;都是类外调用对象的属性,这样是不安全的,因此最稳妥的做法就是利用private实现对属性的封装
将成员属性or方法用private封装之后,只能通过public 构造方法 or 其他方法 来访问or修改
只使用构造方法的话,则成员属性只有默认值,不能更改。因此引入getter()和setter()方法来访问or修改
1.构造方法 有权限,没有返回值,并且名字和类名一致
2.如果声明了构造方法,则不会自动生成一个无参无函数体的构造方法
class Person{
//.....略
/*
*public Person(){}
*默认构造方法,无参无函数体,如果没有定义任何一个构造方法则编译器自动补
*无参但是有函数体则为普通构造~
*/
public Person(Person per){
name = per.name;
age = per.age;
}
//拷贝构造(深拷贝)(深浅拷贝自行搜一下~)
//不是传地址,而是new一个堆空间一一复制
/***下面是两个重载构造方法***/
Person(String Name){
name = Name;
age = -1;
}
Person(String Name , int Age){
name = Name;
age = Age;
}
//.....略
}
Person liu = new Person(zhang);
//这么用的前提是Person类中有定义拷贝构造
liu.tell();
//结果为:姓名:张三、年龄:20
zhang.age = 21;
liu.tell();
//结果还是为:姓名:张三、年龄:20
/*********深拷贝************/
深拷贝:两个对象指向不同空间,互不影响
Person liu = null;
liu = zhang ;//引用传递
zhang.name = "张豪猪";
liu.tell();
//结果为:姓名:张豪猪、年龄:20
/********浅拷贝**********/
浅拷贝:两个对象指向同一空间 ,变化同步
》》四种权限修饰符都是封装性的体现,private是,public protected 缺省的都是
》》权限大小 private<缺省
一张图说明不同权限的可见性范围
把属性封装为private
class Person {
private int age;
private String name;
/*******构造方法的4个重载********/
public Person() {
age = 123456;
name = "未知";
}
public Person(int Age) {
age = Age;
name = "未知";
}
public Person(String Name) {
age = 123456;
name = Name;
}
public Person(int Age, String Name) {
age = Age;
name = Name;
}
public Person(String Name, int Age){
age = Age;
name = Name;
}
/*构造方法的5个重载,包含了1个无参构造、2个单参构造、2个双参构造所有情况*/
/*建议编写构造函数有顺序:参数由多到少or参数由少到多*/
public void tell() {
System.out.println("姓名:" + name + "、年龄:" + age);
}
}
public class Test4 {
public static void main(String[] args) {
Person per1 = new Person();
per1.tell();
Person per2 = new Person(18);
per2.tell();
Person per3 = new Person("张猪豪");
per3.tell();
Person per4 = new Person(20, "张豪猪");
per4.tell();
Person per5 = new Person("窝瓜刘",18);
per5.tell();
/*
* per4.name = 10 ;
* 报错The field Person.name is not visible
*/
}
}
private之后只能通过方法来访问or修改属性成员
per4.setName("我是嫩爹");
System.out.println(per4.getName());
结果:
- 有时候需要在属性的添加删除上加上逻辑,而不进行封装就不能实现,例如
- 创建一个Animals类,属性有legsNumber,而动物的腿都是双数,倘若直接定义一个public legsNumber 就会出现单数的逻辑错误
- 创建一个Account类,属性有balence余额,方法有withdraw取款,而取款必须有“判断余额是否足够”的逻辑
public class Account{
private int balence;
public void withdraw(int qk){
if(balence>=qk){
balence -= qk;
}
else{
System.out.println("余额不足");
}
}
}
在Java中,为了方便管理,提供了自动垃圾收集机制—CG(手动CG和自动CG),即便如此,在编程开发中也要尽量避免产生垃圾以免影响性能
CG:Garbage Collection
Person Zhang = new Person("张三",20);
Person Liu = new Person("刘芒",18);
Liu = Zhang ;
/*****浅拷贝,引用传递****/
由于此时 Liu 原本指向的 堆内存空间heap 没有任何 栈内存stack 对其引用,该内存空间就成为垃圾空间,并且不能再使用,所有垃圾空间将等待CG不定期回收释放
1.只能用一次,用完就成垃圾空间等待回收
2.必须要先有构造方法(不写,用默认的也可以,只是没什么意义)
3.有了构造方法之后就可以在堆内存中开辟的同时进行对象的实例化处理,这样即便是没有栈内存指向,该对象也可以使用一次,而对于这种没有指向的对象就称为匿名对象
4.匿名对象要直接输出或作为返回值
new Person().tell();
new Person("张豪猪",20).tell();
new Person(18,"窝刘瓜").tell();
new Person().setName("瓜皮");//不会报错,但是没有任何意义
System.out.println(new Person().getAge());
匿名对象也能创建之后一直使用,具体操作:
https://blog.csdn.net/m0_56079407/article/details/119322594
类也属于是引用数据类型,所以可以用如下方法来接收类作为形参(代码前面是Person类和Student类的完整定义)
class Person {
private int age;
private String name;
//构造方法
public Person(){}//必须单独写出,不然无参的时候会报错
public Person(int Age) {
age = Age;
}
public Person(String Name) {
name = Name;
}
public Person(int Age, String Name) {
age = Age;
name = Name;
}
public Person(String Name, int Age) {
age = Age;
name = Name;
}
/***** 使用构造方法接受引用类型 ******/
public Person(Student std) {
/**
* age = std.age; name = std.name; 这是错误写法
*,因为Student类种的属性是private
* 必须定义get方法来调用
*/
name = std.getName();
age = std.getAge();
}
/**** 定义get和set方法来访问修改private的属性 ****/
public String getName() {
return name;
}
public void setName(String Name) {
name = Name;
}
public int getAge() {
return age;
}
public void setAge(int Age) {
age = Age;
}
/**** 定义get和set方法来访问修改private的属性 ****/
//其他方法的定义
public void tell() {
System.out.println("姓名:" + name + "\t年龄:" + age);
}
}
class Student {
private String name;
private int age;
private int student_num;
private int class_num;
public Student() {
name = "无名氏";
age = -1;
student_num = -1;
class_num = -1;
}
public Student(String Name, int Age, int Snum, int Cnum) {
name = Name;
age = Age;
student_num = Snum;
class_num = Cnum;
}
// 剩下的几种构造方法重载省略
// 使用构造方法接受引用类型
public Student(Person per) {
name = per.getName();
age = per.getAge();
}
/**** 定义get和set方法来访问修改private的属性 ****/
public String getName() {
return name;
}
public void setName(String Name) {
name = Name;
}
public int getAge() {
return age;
}
public void setAge(int Age) {
age = Age;
}
public int getSnum() {
return student_num;
}
public void setSnum(int Snum) {
student_num = Snum;
}
public int getCnum() {
return class_num;
}
public void setCnum(int Cnum) {
class_num = Cnum;
}
/**** 定义get和set方法来访问修改private的属性 ****/
public void tell() {
System.out.println("姓名:" + name + "\t年龄:" +age+ "\t学号:" + student_num + "\t班级:" + class_num);
}
}
public class Test6 {
public static void main(String[] args) {
Person zhangsan = new Person("张三",18);
Student liumang = new Student("刘芒",20,1438438,3);
Student ZhangSan = new Student(zhangsan);
Person LiuMang = new Person (liumang);
//形参为引用数据类型,具体在类定义中实现
//把形参种的部分属性数据赋值给对象
zhangsan.tell();
liumang.tell();
ZhangSan.tell();
LiuMang.tell();
}
}
在Student类中加入一个方法(开除学生fire)
public void fire(Student std){
System.out.print("这个学生被开除了:\t");
std.tell();
std.name = "已被开除";
std.age = -2;
std.class_num=-2;
std.student_num=-2;
}//new出来的对象可以被使用多次,跳出这个结构后成为垃圾
//因为调用了形参,所以在栈空间中也会创造一
//个空间指向匿名对象,本质还是匿名的,用完就消失
调用的时候必须任意以一个对象为载体使用fire()方法,例如:
liumang.fire(new Student("王老五",18,1582141,3));
//用任意对象调用fire,在fire中new一个对象,fire方法只对new出来的对象操作
liumang.tell();//fire对属性的操作不影响liumang
这种操作不会对载体对象产生影响。
所谓JavaBean就是符合以下条件的Java类:
>类是公共的 public class xxx{}
>有一个无参的公共构造器
>有属性,且有对应的get和set方法
调用方法:
- this.属性 this.方法
- this() this(形参列表)
在类的定义模块中,如果在定义普通方法or构造方法的时候,形参和类属性重名,则需要用this来区分谁是属性谁是形参
public void setName(String Name){
name = Name;
}//形参和属性名不重复,不用加this.
public void setName(String name){
this.name = name;
}//名字冲突,this.name表示类属性
用this.属性 this.方法都是用来表示:
this. 调用当前对象or当前正在创建的对象
- this(形参列表); 必须写在构造器的第一行,且每个构造器最多一个this()
- n个构造器最多可以定义n-1个this(形参),必须留一个“出口”
- 构造器不能用this(形参)调用自己,只能调用“上一级”
上面有一个又臭又长的Person类的定义,不同的构造方法有大部分都是重复内容,因此可以简化为
public void birth(){
System.out.println("已创建一个Person")
}
public Person(){
this.birth();//this.方法的应用,this.可省
}
public Person(String name){
this();
this.name = name;
}
public Person(String name , int age){
this(name);//this()会自动判断形参对应的哪个构造器,然后跳转调用
this.age = age;
}
此时我们创建一个Person类
Person per1 = new Person("张三",18);
此时构造器的调用顺序为:this(name)跳转Person(String name)构造器,在此中第一行又遇到了this()跳转Person()构造器,此中没有this()即为“出口”,从此出依次向外调用回去
package:包,管理项目中众多类的集合,按功能写不同的包,每个包中有很多类,以此完成特定的独自的功能。在编译器中首行声明 package ZJH.HJZ.bankTest;
切记,不能用关键词命名包名,否则运行的时候会报错eg: package ZJH.HJZ.java;
import: 使用import导入指定包下的类or接口
先定义父类的4种不同权限的属性和方法:
public class Super {//public类
private int Pri;
int Def;
protected int Pro;
public int Pub;
//四种权限的属性
public Super(){}
public Super(int Pri, int Def , int Pro, int Pub){
this.Pri=Pri;
this.Def=Def;
this.Pro=Pro;
this.Pub=Pub;
}
//构造
private void PriTest(){
System.out.println("private");
}
void DefTest(){
System.out.println("defualt");
}
protected void ProTest(){
System.out.println("protected");
}
public void PubTest(){
System.out.println("public");
}
//四种权限的方法
}
然后再同一个包下创建主类Test一下:
同一个包下,父类除了private,其他3个对子类都可见,要想访问父类种的private属性or方法,可以把private放在子类能访问的方法中
public void DefTest(){
System.out.println("defualt");
PriTest();
}
角度1:
角度2:
总结:
- 权限大小:public>protected>缺省>private
- 每“出一层”,可见范围少一个
同包建立任意类(子类,普通类):private不可见了
跨包建立子类:缺省也不可见了
跨包建立普通类(非子类):protected也不可见了
实际开发中一般只用public和private
public类可以给同一工程下所有包用
缺省类只能给本包用
可见可以变成不可见,不可见可以变成可见,关键由最后一次是如何继承的决定。
1.Java中只允许多层继承,不允许多继承:一个父类可以继承给多个子类,但是一个子类只能继承于一个父类
2.继承层次不要太多,一般不建议超过3层,以免阅读困难
*3.所有没有显式继承的类,都继承于java.lang.Object类,由于“孙类”依然有父类的全部功能,所以所有的类都有Object类的所有功能
4.一旦继承,就继承了所有的属性和方法,只是由于权限封装性的影响,有些内容不可见,不能直接调用,但是可以间接调用(在可见方法中写入不可见方法)
子类对象在实例化之前一定会实例化父类对象,实际上这个时候就相当于子类的构造方法里面隐含了一个super()的形式
super(形参列表)必须在子类构造方法的第一行
父类构造方法:
public Super(){
System.out.println("调用父类了无参构造");
}
子类构造方法:无参的super()可省
public class Sub extends Super {
public Sub(){//子类无参构造
super();//明确指出调用父类构造,不编写则默认找到父类无参构造
System.out.println("调用了子类无参构造");
}
}
测试:
public class Test {
public static void main(String[] args) {
Sub A = new Sub();
}
}
当子类定义有参的构造方法时,super(形参列表)必须与父类构造一致
this()和super()都是构造方法,并且都要求放在第一行,但是this()和super()只会同时出现其中一个,一般用this(形参)针对父类构造,super(形参)针对子类构造
作用和this(形参列表)相似,不过super(形参列表)寻找的是父类的构造函数
class Person{
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
}
class Student extends Person{
private int id;
public Student(String name, int age, int id) {
//形参要求齐全
super(name, age);//先super()构造
this.id = id;
}
}
无论父类的属性是什么权限,子类的构造必须使用super(形参列表),即便是public属性,也不能在子类构造函数中用this.和super.对继承来的属性初始化
子类对象实例化的
方法重载和重写(覆写)的区别:
重载:只要求形参列表相同,返回值,可见性都可以不同
重写:父子类中形参列表相同,返回值也有要求
父类方法返回A类型,子类重写返回A或A的子类
其他的返回值必须相同void->void int->int double->double......
覆写之后不明确用super.方法(),则只调用覆写之后的方法
覆写的意义
保证父子类结构一致的前提下保留名称,并给与子类方法独特的个性。
方法覆写一般直接复制粘贴,只改动{ }中间的函数体,因为 权限 返回值 名称都要求相同,并且static的覆写也需要加static
super.方法()虽然是super,但是不同于构造中的super(方法),覆写的super.方法()可以写在子类中的任意位置
class Super{
public void tell(){
System.out.println("我是你爹");
}
}
class Sub extends Super{
public void tell(){//覆写
System.out.println("***********");
super.tell();//调用父类的被覆写的方法
super.tell();//可以调用多次,并且位置不限
System.out.println("我是你儿子");
}
}
public class Test {
public static void main(String[] args) {
Sub A = new Sub();
A.tell();
}
}
1.权限修饰符:子类的覆写方法 >= 父类被覆写的方法
就像摊煎饼,要覆盖上一层的煎饼,不能用一个更小的煎饼来覆盖
2.如果父类的是private方法,则不能覆写,即便是覆写格式正确,也只是在子类中定义一个新的方法(父类的private在子类中不可见,覆写的前提是可见)
class Super1 {
int a = 1;
public int getA() {
return this.a;
}
}
class Sub1 extends Super1 {
String a = "qwe";
// 因为属性覆写只要求 名字相同,不要求类型
//所以可写 int a = 2;
public String getAAAAAA() {
return this.a;
//this取本对象的a
}
public int getBBBBBB() {
return super.a;
//super取父类的a
}
}
public class Test2 {
public static void main(String[] args) {
Sub1 S = new Sub1();
System.out.println(S.getAAAAAA());
System.out.println(S.getBBBBBB());
}
}
规律:
开发中一般不会同名
多态性是一个运行期行为,不是编译期行为,编译器看不出来有没有多态,总结会有详细解释
先定义一个父类Person—>子类Man—>“孙类”Boy
public class Person {
int age;
String name;
public void eat(){
System.out.println("人要吃饭");
}
}
public class Man extends Person{
boolean isSmoking;
public void eat(){
System.out.println("男人吃的多");
}
public void Drink(){
System.out.println("男人要喝酒");
}
}
public class Boy extends Man{
public void eat(){
System.out.println("小孩要吃的有营养");
}
public void Drink(){
System.out.println("小孩不能喝酒");
}
}
然后Test一下
- 编译看左,运行看右:(虚拟方法调用)
Person A = new Man(); 左边决定能用什么属性or方法,右边决定用谁的属性or方法
在编译期只能调用父类中声明的方法,但在执行期实际执行的是子类重写的方法
- 左边父类,右边子孙:
父类引用指向子类对象,子类对象赋给父类引用,左边范围小,右边范围大
如果左边是子类,右边是父类就会报错
public class Test {
public static void main(String[] args) {
Person A = new Man();
A.eat(); //男人吃的多
//A.Drink();报错
//在编译期只能调用父类中声明的方法,但在执行期实际执行的是子类重写的方法
//编译看左,执行看右
Person B = new Boy();
B.eat();//小孩要吃的有营养
//B.Drink();报错
//在编译期只能调用父类中声明的方法,但在执行期实际执行的是子类重写的方法
//编译看左,执行看右
Man C = new Boy();
C.eat();//小孩要吃的有营养
C.Drink();//小孩不能喝酒
//Boy D = new Person();报错
//在编译期只能调用父类中声明的方法,但在执行期实际执行的是子类重写的方法
//编译看左,执行看右
}
}
如果没有对象多态性,则只能在三个类中创建三个不同的eat(){} drink(){}方法,通过形参不同来实现重载。多态性的存在使相似功能的实现变得简洁。
instanceof方法
a instanceof A,若a是A的对象,则返回true否则返回false。a若是A的对象,对于A的父类 a instanceof BaseA也为true,并且最终a instanceif Object一定成立
举例说明对象方法多态性
class Base{ public void tell(int a,int...b){ System.out.println("BASEbase"); } } class Sub extends Base{ public void tell(int a,int[] b){ System.out.println("SUBsub"); } } public class GeometicTest { public static void main(String[] args) { //编译看左,执行看右 Base A = new Sub();//编译角度认为A是Base类 Base B = A;//编译角度认为B是Base类 //Base B = (Base)A; //Base B = (Sub)A; //三者效果都一样,子类到父类是自动转,不用显式声明 //编译角度认为B是Base类,编译格式要满足Base的格式要求 B.tell(1,2,3);//执行角度认为B是Sub类 Base C = (Base)A;//可以不要这个(Base),C指向A的地址 C.tell(1,2,3);//执行实际上执行的是A Sub类 Sub D = (Sub)A;//编译角度认为A是Base类,所以需要强制类型转换 D.tell(1,new int[]{1,2,3});//执行Sub的方法 System.out.println(A instanceof Object); System.out.println(A instanceof Base); System.out.println(A instanceof Sub);//运行角度认为是Sub类 System.out.println(B instanceof Sub); System.out.println(C instanceof Sub); System.out.println(D instanceof Sub); } }
class Person{
int age = 1;
public Person(){
age = 2222;
}
}
class Man extends Person{
int age ;
//只要在子类又定义了age,则触发“编译运行都看左”的规律
//因为此时堆空间中有两个age,在实际调用的过程中调用父类的age
public Man(){
//这个构造函数无论怎么写,都无法改变调用结果
}
}
下面这个例子在子类重定义了age,所以调用值调用父类的属性or方法
class Person{
int age = 1;
//构造略
}
class Man{
int age = 2;
//构造略
}
class Test{
public static void main(String[] args){
Person A = new Man();//构造函数调用的是左边Person类的
System.out.println(A.age);//结果为1
}
}
则堆空间只有唯一一个age,这个age受到所有能影响他的因素改变
Person A = new Man();调用的是Person的构造方法,原理如下:
不在子类重新定义int age 则不构成多态,任何方法都能访问修改唯一的age值
class Person{ int age; //空参构造可以省 } class Man extends Person{ public Man(){ //此处系统自动补全super(); this.age = 99; } } class Boy extends Man{//构造可省 } class Test{ public static void main(String[] args){ Person A = new Boy();//实例化一个Boy,把值赋给Person System.out.println(A.age)//结果为99 } }
无关构造函数带不带参,总之就是super()之后的this.属性=XXX操作会直接影响最终属性值
new的是谁就调用谁的构造方法,只是构造方法内部会调用上一级的构造方法(Boy调用Man调用Person),如果不在super()语句之后再添语句加以修改属性值(age=99),最终体现出来的就是最后一次调用的结果(Person的默认age=0)
由此可知,子类实例化赋值给父类,子类的属性,方法都可以覆盖父类,方法比较浅显易懂,而属性的覆盖需要额外在super()之后声明
对象多态性≈重定义
重定义方法:编译看左,执行看右
重定义属性:编译执行都看左
堆空间生成两个同名属性age,父类方法决定父类age,子类方法决定子类age,最终调用的是父类的age
虚拟方法调用VMI
虚拟方法调用(Virtual Method Invocation)正常的方法调用
Person A = new Person(); A.eat();
Man B = new Man(); B.eat();
虚拟方法调用(多态情况下):
Person A = new Man(); A.eat();
子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。
编译时A为Person类型,而方法的调用是在运行时确定的,所以调用的是Man类的eat()方法。——动态绑定
多态性是一个运行期行为,编译器看不出来
从编译和运行的角度看:
重载,是指允许存在多个同名方法,而这些方法的参数不同。编译器根据方法不同的参数表,对同名方法法名称做修饰。对于编译器而言,这些同名方法就成了不同的方法。它们的调用地址在编译期就绑定了。Java的重载是可以包括父类和子类的,即子类可以重载父类的同名不同参数的方法。
所以:对于重载而言,在方法调用之前,编译器就已经确定了所要调用的方法,这称为“早绑定”或“静态绑定”;
而对于多态,只有等到方法调用的那一刻,编译器才会确定所要调用的具体方法这称为“晚绑定”或“动态绑定”。
引用一句Bruce Eckel的话:“不要犯傻,如果它不是晚绑定,它就不是多态。”
因为Java是一个完全的面向对象的语言,几乎所有的方法都可以直接通过对象.方法()调用,然而8种基本数据类型的存在就很鸡肋,不能直接用int. char. double.来调用想用的功能,也没有继承封装等面向对象的思想,因此引入了包装类:
除了int和char,其他的都是直接首字母大写,前6个数值型的包装类都有一个共同的父类Number,Number又继承于Object
这8种包装类都有这类似的一句“提供String转换方法”,即在初始化构造的时候可以在()种输入String类型,并且每个包装类都有独自的转换方式
Boolean例子
public class WrapTest {
public static void main(String[] args) {
Boolean A = new Boolean(true);//true
//加不加双引号都行,加了就自动String转boolean
Boolean B = new Boolean("TrUe");//ture
//Boolean包装类定义构造的时候忽略大小写
Boolean C = new Boolean("qwer123");//true
//其他的所有情况都是false
System.out.println(A.toString()+B.toString()+C.toString());
}
}
Float例子
Float的()中可以放double float String三种类型
Float A = new Float(12.3);
//double自动转成float
Float A2 = new Float(12.3f);
Float B = new Float("12.3");
//String自动转成float
Float C = new Float("12.3aaa");
//编译正常,运行报错,Float没有定义怎么处理字母
包装类是个类,因此默认值是null。基本数据类型有自己的默认值
class Order{
int a ;
Integer b;
}
//测试
System.out.println(new Order().a);//0
System.out.println(new Order().b);//null
//这里无论什么包装类,默认值都是null
例如Float包装类的对象在调用float值的时候会自动调用floatValue()方法,也可以自己显式表达
Float B = new Float("12.3");
System.out.println(B.toString()+1);//12.31
//toString出来的是String类型,+1是一个连接符+1
System.out.println(B.floatValue()+1);//13.3
System.out.println(B+1);//13.3
//B+1是自动调用成B.floatValue()+1
同理:Integer对应的就是B.intValue()
基本数据类型 —— 装箱 ——> 包装类
可以直接把基本数据类型赋值给包装类,而不用new一个出来,例如
Integer A = 1;
相当于
Integer A = new Integer(1);
根据这个特性,可以定义方法method(Object obj){ } 形参可以直接放数字1,数字1会自动转换为Integer包装类
由此可见,这是一个正确的表达式(true)
System.out.println(new Integer("111").equals(111));
System.out.println(new String("aaa").equals("aaa"));
包装类 —— 装箱 ——> 基本数据类型
可以不用xxValue方法,直接取值
Integer A = 1;
int a = A;
相当于
Integer A = new Integer(1);
int a = A.intValue();
1, +“”转换法:
2,String.Valueof(基本数据类型or包装类)转换法:
注意区分一下String.Valueof(xx)和XX.intValue()格式上的差别
Integer A = 111;//int A = 111;也一样
System.out.println(A+""+123);
System.out.println(String.valueOf(A)+123);
//两个结果都为111123,因此发生了字符串连接
包装类.parse包装类名(String)方法
Integer A = Integer.parseInt((new String("111")));//111
Double B = Double.parseDouble(A.toString());//111.0
Float C = Float.parseFloat("111");//111.0
Boolean D = Boolean.parseBoolean("true");//D就是true
为什么不能直接强转成String类型?
- 不同于8种基本数据类型之间可以强转,包装类是类,必须要有子父类关系才能强转(子转父)
- Integer A = (Integer)(new String("1111"));这类表达是错误的
class Circle{
int r;
public Circle(int r){
this.r=r;
}
System.out.println(new Circle(1) instanceof Circle);
//true
toString是定义在java.lang.Object的方法,因此所有类都可以使用toString方法,但是
class Circle{
int r;
public Circle(int r){
this.r=r;
}
//具体实现
System.out.println(a.toString());
System.out.println(new Circle(1).toString());
System.out.println(new Object().toString());
System.out.println(new Date(4151211L).toString());
System.out.println(new Integer(22).toString());
前三个输出地址,后两个因为JDK默认了重写,输出的是实体信息
再次运行
public String toString() {
return "Circle [r=" + r + "]";
}
用static定义的方法只能调用static属性or static方法
普通方法则都可以调用
- 用static定义的方法归整个类所有,可以在实例化之前调用
- 用static定义的方法不能出现this. 因为this表示本对象,也不能出现super关键字
class Person{
static String nation;
int age;
public static void setNation(String Setnation){
nation = Setnation;//绝对不能加this.nation
//static方法内不能访问age这种非static属性
}
}
public class Test{
Person.nation = "China";//实例化之前直接用类名访问static属性
Person.setNation("中国");//实例化之前直接用类名访问static方法
}
理解:在实例化之前只能调用类属性和类方法,此时是没有加载普通属性和普通方法的,因此不能对普通属性和方法进行访问,所以static方法不能访问普通属性和方法。
final关键字可以实现以下三个功能
- 定义不能继承的类
如String System StringBuffer类
- 不能重写的方法
如Object类中的getclass()
- 定义常量(全局常量)
- (ps:final和abstract不相容)
final关键字可以先声明后定义
final a; a = 1; //此后a就不能变了 //a=1之前不能调用,会报错
public final class NoInherit{ }
//不能继承的类
public static final void tell(){}
//不能重写的方法
static final private int a;
//静态常量,final一般都是跟static连用
用来初始化属性信息
可以用来初始化属性和一些输出语句
每次实例化的时候调用一次,并且优先于构造方法
int a = 1;和 int a ;{ a = 2}地位等价,执行顺序由声明顺序决定
可以用来初始化属性和一些输出语句
new的时候直接调用,并且只调用一次
可以用static{ 对static属性初始化 }
静态代码块—>普通代码块—>构造器—>普通代码块—>构造器—>普通代码块—>构造器—>...
public class PersonTest{
public static void main(String[] args) {
new Person();
new Person();
}
}
class Person{
public Person(){
System.out.println("构造器");
}
{
System.out.println("代码块");
}
static{
System.out.println("静态代码块");
}
}
当创建Student类继承于Person类的时候,在Student类中也构造Static{}普通{}和构造器。顺序如下
先父后子,静态先行
Person静态代码块—>Student静态代码块—>Person{}、Person构造器—>Student{}、Student构造器
父类不需要造对象,父类所含的方法都很抽象,不精细
子类才造对象,子类的属性 方法分的很细,适合解决问题
此时把父类定义为abstract class抽象类,并且抽象概括的方法都定义为abstract,需要在子类重写后使用,抽象方法没有方法体(只声明)
子类要把所有abstract方法重写,不然自己也是abstract类
有抽象方法则必须为抽象类,反之则不需要
abstract不能修饰private方法、static方法、final方法
抽象类中如果定义static方法则可以不受限制,不new的时候调用static方法
一个方法有多个修饰符时,修饰符之间顺序不影响public static = static public
以下例子融合了上面许多知识点
abstract class Person {
int age;
String name;
static String country;
static{
country = "中国";
System.out.println("这里的Person都是中国人");
}
public abstract void tell();
static public void says(){
System.out.println("没有限制,可以直接调用");
}
public Person(){}
public Person(int age,String name){
this.age=age;
this.name=name;
}
}
class Student extends Person{
static String school = "哈尔滨佛学院";
public Student(){
super();
}
public Student(int age,String name){
super(age,name);
}
public void tell(){
System.out.println("调用了tell方法");
}//重写父类abstract方法
}
public class ABTest1 {
public static void main(String[] args) {
Person.says();
System.out.println("**********************");
// test1(new Person());报错,抽象Person不能new
test1(new Student());
// test2(new Person());报错,抽象Person不能new
test2(new Student());
// test3(new Person());报错,抽象Person不能new
test3(new Student());
System.out.println("**********************");
test2(new Person(){
public void tell(){
System.out.println("调用了匿名对象重写的tell");}
//这里只是重写,结果并没有输出
}
);
//在匿名对象内部重写abstract方法就不会报错
}
static public void test1(Object obj){
System.out.println("1测试成功");
}
public static void test2(Person per){
Person.says();
per.tell();//多态性
System.out.println("2测试成功");
}
static public void test3(Student stu){
Student.says();
stu.tell();
System.out.println("3测试成功");
}
}
new Person(){
里面把所有abstract类全部重写
}
重写之后的方法如果需要调用,则调用的是重写后的方法
类作为属性可以当作普通属性来构造,调用
主类实现时,类作为属性可以直接在形参列表new一个匿名对象
类数组定义后只能存放该类对象or子类对象
class Mydate {
private int year;
private int mounth;
private int day;
......
}
class Person{
private int name;
private Mydate birthday;
//类作为属性,不是继承
public Person(int name,Mydate birthday){
this.name = name;
this.birthday = birthday;
//类作为属性,可以用this调用
......
}
}
class Student extends Person{
......
}
public class Test{
public static void main(String[] args){
Person[] perArr = new Person[3];
//创建一个长度为3的数组perArr,每个位置都必须赋值为Person或其子类的对象
perArr[0] = new Person("张三",new Mydate(2021,8,1));
//赋值为Person类,同时形参列表中可以创建匿名对象
perArr[1] = new Student("刘逼",1001,new Mydate(2021,8,2));
//多态性,赋值一个Student类
}
}
- Java不支持多继承,因此引入了一个和类并列的结构接口,接口可以达到多继承的效果。继承的判断是“A is a B”则class A extends B,接口则不是is a关系,例如:键盘、鼠标、u盘都支持USB连接,但不能说他们is a USB连接
- 继承的是“是不是”关系,而接口实现的是“能不能”的关系
- 接口的本质是契约、标准、规范
接口定义
JDK7之前只能定义以下两个
public interface 接口名{
public static final 类型 属性 = XX;public static final可省
public abstract 返回值类型 方法(); abstract可省
}
解释:因为接口只有全局常量和抽象方法,所以可以省略
接口没有构造器,所以不能实例化
子接口可以用extends继承父接口
子类可以通过implements连接父接口
接口的继承
public interface 子接口 extends 父接口{
一些补充方法
}
可实例化的类,接口通过implements实现
public class 子类 implements 父接口名,父接口名,父接口名{
在这里面重写所有的父接口方法
}
只重写部分父接口方法,该类为抽象类
public abstract class 子类 implements 父接口名,父接口名,父接口名{
在这里面重写所有的父接口方法
}
同时继承类也继承接口
public class 子类 extend 父类 implements 父接口名,父接口名,父接口名{
在这里面重写所有的父接口方法and父类方法
缺一个没重写就是abstract类
}
先定义两个接口,两个接口规范了实现类的必要条件,
interface Flyable {
public static final int MAX_SPEED = 50 ;
int MIN_SPEED = 10;//省略了public static final
public abstract void fly();
}
interface Eatable {
void eat();//省略了public abstract
}
abstract class Bird implements Flyable{
public void fly(){
System.out.println("鸟会飞");
}
}
class Bird_EatBugs extends Bird implements Flyable{
public void eat(){
System.out.println("有一些鸟吃虫子");
}
}
class Eagle implements Flyable,Eatable {
public void fly(){
System.out.println("老鹰是大鸟,也能飞");
}
public void eat(){
System.out.println("老鹰吃肉");
}
}
public class FlyTest {
public static void main(String[] args) {
Bird B1 = new Bird_EatBugs();
// Bird_EatBugs.fly()不能调用,因为编译看左
B1.fly();//抽象类只是不能实例化,并不是不能编译
Bird_EatBugs B2 = new Bird_EatBugs();
B2.fly();
B2.eat();
Eagle E1 = new Eagle();
E1.fly();
E1.eat();
}
}
类可以有匿名子类,通过new 父类 {重写} 实现
接口也有匿名子类,通过 new 接口 {重写} 实现
Flyable FFF = new Flyable(){//new一个名为FFF的对象
public void fly(){
System.out.println("不知名的可飞行生物");
}//匿名内部需要重写
};//此时本质上new的是重写后的子类,不过没有名字
FFF.fly();
public static void Method(Object obj){
System.out.println("调用成功,接口的匿名子类也属于Object");
}
Method(new Flyable(){
public void fly(){
System.out.println("不知名的可飞行生物");
}
});
这个版本之后的接口可以定义4种内容
- public static 接口静态方法
- public static fianl 全局常量
- public default 接口默认方法
- public abstract 用于规范的抽象方法
这些结构一般能简化,但是开发中建议不要简化,只是应注意在面试中可能会遇到挖坑的情况
下面用一个例子来完整说明4个方法的用法,定义两个接口Bounceable和Rollable来规范一个可以弹也可以滚的东西,定义他们的实现类Ball,定义一个继承于Ball的Basketball类
interface Rollable {
public static final double PI = 3.1415926535;
public abstract void roll();// 抽象方法,等待重写
public static void tell() {//解释这个接口的目的
System.out.println("这个东西能够滚动");
}//
public default void check() {//检查能否滚动
System.out.println("能调用,这个类能滚动");
}
}
interface Bounceable {
int MIN_BOUNCE = 1;//省略public static final,表示最小能弹1下
void bounce();//省略public abstract,等待重写
public static void tell(){//静态方法可以由接口直接调用,所以重名不用重写
System.out.println("这个东西能弹起来");
}
public default void check() {//default方法由对象调用,重名需重写
//多接口情况下接口同名同参方法必须在实现类重写,否则接口冲突,报错
System.out.println("能调用,这个类能弹起来");
}
}
class Ball implements Bounceable,Rollable{
@Override
public void roll() {
System.out.println("ball可以roll");
}
public void bounce() {
System.out.println("ball可以bounce");
}
@Override
public void check() {//default方法只能定义在interface中
//通过如下重写,让check方法只能检测bounce功能
//格式就是 接口.super.重名的默认方法();
Bounceable.super.check();
}
}
class Basketball extends Ball{//继承了Ball类就继承了Ball的所有接口
public void roll(){
System.out.println("篮球滚的很远");
}
public void bounce(){
System.out.println("篮球弹性很好");
}
//因为父类已经重写过了check方法,所以子类可以不用重写check
public void check(){
// Rollable.super.check();
// Basketball没有直接implements两个接口,所以不能用这个方法调用
System.out.println("篮球是弹的又高,滚的又远");
}
}
public class InterTest {
public static void main(String[] args) {
System.out.println(Basketball.MIN_BOUNCE+"\t"+Basketball.PI);
//接口静态常量,只要跟他有关的下级都能直接调用
Rollable.tell();
Bounceable.tell();
//接口静态方法,只能用接口调用,与类类似
System.out.println("*******************");
Basketball bsk = new Basketball();
bsk.bounce();
bsk.roll();
//调用两个在实现类中重写的方法
bsk.check();
System.out.println("*******************");
Ball bo = new Ball();
bo.bounce();
bo.roll();
bo.check();
}
}
类优先原则
如果Ball定义为abstract,则可以删除Ball中重写的roll()和bounce()方法
如果Ball中重写了roll()和bounce()方法,则子类Basketball调用的是Ball中重写的
Java中允许在一个类A中声明另一个类B,则B叫内部类,A叫外部类
内部类分为:成员内部类(静态or非静态)、局部内部类(方法内or代码块内or构造器内)
一方面:
另一方面:
JVM的类加载规则 :
1. static类型的属性和方法,在类加载的时候就会存在于内存中。
2. 要想使用某个类的static属性和方法,那么这个类必须要加载到JAVA虚拟机中。
3. 非静态内部类并不随外部类一起加载,只有在实例化外部类之后才会加载。
现在考虑这个情况:在外部类并没有实例化,内部类还没有加载,这时候如果
调用内部类的静态成员或方法,内部类还没有加载,却试图在内存中创建该内
部类的静态成员,这明显是矛盾的。所以非静态内部类不能有静态成员变量或
静态方法。
假设 :在外部类并没有实例化,内部类还没有加载,这时候如果JVM加载静
态成员或方法,内部类还没有加载,因为非静态内部类的加载依赖于实例化,
而此时却试图在内存中创建该内部类的静态成员,这明显是矛盾的。所以非
静态内部类不能有静态成员变量或静态方法。
下面是一个测试,Person类有宠物Cat、Bird、抽象内部类Dog和继承后的PetDog
解决了如下问题:
如何实例化内部类对象
如何区分内部类和普通属性 方法
class Person{
String name;
int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public static void GetCountry(){
System.out.println("这里是中国");
}
public void Language(){
System.out.println("我们说中文");
}
abstract class Dog{
String name;
public Dog(String name){
this.name = name;//这里的this指代Dog
}
public void call(){
System.out.println("汪汪汪");
}
}
class PetDog extends Dog{//内部类也可以继承
public PetDog(String name) {
super(name);
}
public void check(){
Language();//这个不能用Person调,因为是非静态,要用对象调
GetCountry();//省略
Person.GetCountry();//省略
Person.this.GetCountry();
//不能调用Person.Language()非静态方法
//因为不能用类来调用非静态方法
System.out.println(age);//无重名,省略
// System.out.println(this.age);
// 必须明确指出调外部类的age
System.out.println(Person.this.age);//外部类
System.out.println(Person.this.name);//外部类
System.out.println(name);//直接调是调重写后的内部类name
System.out.println(this.name);//直接调是调重写后的内部类name
}
}
final class Cat{
String name;
//非静态内部类都不能声明static属性or方法3
public Cat(String name ){
this.name = name;
}
public void call(){
System.out.println("喵喵喵");
}
}
static class Bird{
static public void call(){
System.out.println("嘎嘎嘎");
}
}
}
public class InnerTest {
public static void main(String[] args) {
Person.Bird.call();
Person.Bird bird = new Person.Bird();
bird.call();
//Person.静态内部类.静态方法
Person per = new Person("张三",18);
//per调不了内部类
// Person.Cat kitty = new Person.Cat("KITTY");
//因为Cat不是静态内部类,所以不能用Person调用
//应该用Person的实例化对象来调用非静态内部类Cat
Person.Cat kitty = per.new Cat("KITTY");
kitty.call();
Person.PetDog NIKI = per.new PetDog("NIKI");
NIKI.check();
}
}
在外部类的方法中定义一个内部类
public class Person{
//......
//属性、方法、构造器、get set方法略
//......
public Comparable getComparable(){
//创建一个实现了Comparable接口的类:局部内部类
class MyComparable implements Comparable{
@override
public int compareTo(Object obj){
return 0;
}
}
return new MyComparable();
}
}
//方式二:创建实现Comparable接口的匿名实现类的匿名对象
class MyComparable implements Comparable{
return new Comparable(){
@override
public int compareTo(Object obj){
return 0;
}
}
}
有些问题不能光靠代码就能避免,比如客户输入数据的格式错误,客户访问的文件不存在,网络问题等等,如果客户在遇到这些问题的时候只能看到一堆报错的英文会大大降低用户体验。通过异常处理可以提供出现问题的解决措施。
就像是扶贫工作也不能完全消除贫困现象,因为总有些人你去帮他他也不愿意付出劳动,异常处理也是一样,总会有一些问题不能解决,但是都是少数。
异常处理就是对可能出现的问题进行预处理,通过编写异常处理代码来应对“当....时,就....”的异常情况
Error
Java虚拟机无法解决的严重问题,如JVM系统内部错误、资源耗尽等严重情况。如:StackOverflowError栈溢出 和 OutOfMemoryError堆溢出OOM ,一般不编写针对性的代码进行处理(异常处理代码),而只是对原本代码进行改动。
Exception
其他编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理,不改动原有代码,只增加异常预处理,如:
- 格式异常 NumberFormatException
- 空指针访问 NullPointerException
- 试图读取不存在的文件 FileNotFoundException
- 网络连接中断 ConnectException
- 数组角标越界 ArrayIndexOutOfBoundsException
error一般也可叫作“错误”,异常一般都是针对Exception,这种异常可以编写逻辑代码进行处理,异常分为:编译时异常、运行时异常
e.toString(): 获得异常种类和错误信息
e.getMessage():获得错误信息
e.printStackTrace():在控制台打印出异常种类,错误信息和出错位置等
例如要处理X/Y,则要考虑分母为0、数据输入格式不是数字、数据为空等等情况,如果大量使用if-else语句则会过长可读性会降低,因此采用异常处理
java异常处理是将需要异常处理的代码集中在一起,与正常的程序代码分开,使程序简洁,利于维护。
- 过程一:抛:程序正常运行过程中,一旦出现异常,就会在异常处生成一个异常类的对象并抛出,一旦抛出后,异常之后的代码就不再运行
- 过程二:抓:可以理解为异常处理的方式,一般有两种:try-catch-finally \ throws
- 这个方法类似于if-else语句,finally好比default,不过是无论对错都会执行
- 将可能出现异常代码用try{}包装,出现异常则与catch(){}匹配,一旦处理完成or没找到匹配,则跳转finally{},如果没有则跳出整个结构
- catch异常类型如果没有子父类关系,则顺序任意
- catch异常类型如果有子父类关系,则父类绝对不能在子类上面
- 该结构内声明的变量是try-catch-finally结构内部的局部变量,并且可以对全局变量进行访问修改
try{ 可能出错的语句1 可能出错的语句2... }catch(xxxxException e){ 处理语句1 }cathch(yyyyException e){ 处理语句2 }cathch(zzzzException e){ 处理语句3 }finally{ 无论对错,最终都执行的语句 如果没有进入try-catch结构,则不执行finally }
例1
Scanner scan = new Scanner(System.in);
try{
int temp = Integer.parseInt(scan.next());
System.out.println(temp);
}catch(NumberFormatException e){//异常后面随便加个对象
System.out.println(e.getMessage());
//获取错误信息
System.out.println(e.toString());
//获取异常类型和错误信息
System.out.println("格式错误");
}catch(RuntimeException e ){
System.out.println("运行错误");
}finally{
System.out.println("无论对错,最后一定执行");
}
例二:文章最开头写到的X/Y结果的try-catch结构
int x = 0;
int y = 0;
Scanner scan = new Scanner(System.in);
x = scan.nextInt();
y = scan.nextInt();
try{double temp = (double)(x)/y;
System.out.println(temp);
}catch(InputMismatchException e){
System.out.println("输入数据格式不正确或超出范围");
System.out.println(e.getMessage());
System.out.println(e.toString());
}catch(ArithmeticException e){
System.out.println("不符合数学规则,分母不能为0");
System.out.println(e.getMessage());
System.out.println(e.toString());
}catch(Exception e){
System.out.println("请重新尝试");
System.out.println(e.getMessage());
System.out.println(e.toString());
}finally{
System.out.println("结束");
}
没有进入catch的原因如下
java中的double是有特殊值的,NaN 和 Infinite,可以通过isNaN和 isInfinite来判断。既然java的double支持这两个值,那么除以0在double中就不是异常。
0d / 0d 结果为 NaN
非0 / 0d 结果为 Infinite ( 注意正负 )
注意我说的是double, 如果是 int / int 的话,还是有除0异常的,因为int是不支持这两个特殊值的。
int 与 double运算,会自动转为可表达的数字范围更广的double类型将(double)(x)/y的double去掉,则表现如下
什么时候一定要finally?
满足①:
- try{}的末尾要return
- catch{}中出现异常,并且catch需要return(catch内部异常可以根据需要嵌套一个try-catch-finally)
- 防止出现无return的情况,添加finally{并且return}
或满足②:
- 像数据库连接、IO流、网络编程Socket等资源,JVM不能自动回收,需要在finally手动回收进行资源释放
throws抛异常实际上没有给出对应异常的处理办法,只是把遇到的异常向上提交。例如:method3在method2内定义,method2在method1内定义,那么3遇到的异常可以通过thorws交给2处理,2可以处理也可以把异常再向上提交给1处理,到最外层之后就最好用try-catch-finally而不是向上抛了。
假设最内层的method3只throws了指定的甲乙两个异常,那么method2只能抛出包含甲乙or其父类的更多的异常,同时method1只能针对明确指出的throws出来的异常进行try-catch-finally处理(被调用的只管抛,由调用方处理)
例1:
public class ThorwsTest {
public static void main(String[] args) {
try {
new ThrowsT().method3();
} catch(InputMismatchException e){
e.printStackTrace();
}finally{
System.out.println("finally一定能运行");
}
class ThrowsT{
static Scanner scan = new Scanner(System.in);
public static int method1() throws RuntimeException,NoSuchElementException,InputMismatchException{
//内层method1,抛出的3个异常有子父类关系,没有影响
int a = new Integer(scan.nextInt()).intValue();
return a;
}
public void method2() throws InputMismatchException{
System.out.println("method1方法输入的是:"+method1());
System.out.println("嵌套调用,method2接受来自method1抛出的异常");
}
public void method3()throws NoSuchElementException{
new ThrowsT().method2();
System.out.println("嵌套调用,method3接受来自method2抛出的异常");
}
}
甲写了一个方法method1,并对乙说:“我写了method1方法给你用,可能出现xxx yyy 这两种BUG,你记得自己用try-catch处理一下。”
乙写了个method2调用这个method1,为了防止出现异常导致整个程序挂掉,于是用try-catch把这个method1方法给框起来处理。 如果乙自己不使用,而只是再写一个方法交给其他人使用,那么依然可以只throws异常而不处理
丙、丁最终拿到乙的method2方法来调用,由于乙throws出了几种类型的异常,根据调用者的不同,给与不同开发者自主用try-catch处理异常的权利
假如丙、丁所调用的乙的方法method2并没有throws出异常类型,虽然不会报错,但这对丙、丁开发者十分不友好,丙、丁不知道调用了method2之后到底会出现什么异常,连try-catch的方向、思路都没有
这样做就只需要调用者根据需要处理不同的异常,而被调用者只需要给出“可能会出现什么异常”以供调用者一个异常处理思路
try-catch和throws不共存,当try-catch已经把异常处理好了,再throws没有意义
class Super{
Scanner scan = new Scanner(System.in);
public void methodA()throws RuntimeException{
//父类抛的异常范围更大
String s = scan.next();
int i = new Integer(s).intValue();
System.out.println(i);
}
}
class Sub extends Super{
public void methodA() throws InputMismatchException{
//子类重写抛的异常范围更小
System.out.println("重写");
}
}
public class TTTTT {
public static void main(String[] args) {
methodTest(new Sub());
}
public static void methodTest(Super su){
//形参多态性,try-catch结构由父类throws类型决定
try {
su.methodA();
} catch (RuntimeException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
区别与throws,手动抛出是没有s的
并且抛出的是一个异常类的对象,必须要new
以抛出 throw new RuntimeException() 为例,这个类有一个构造器,对属性message进行初始化,空参构造则getMessage()返回的是null
public class ThrowTest {
public static void main(String[] args) {
try {
Student stu = new Student(-1225);
System.out.println(stu.toString());
} catch (Exception e) {
//这里可以Exception
//也可以Throwable
//还可以RuntimeException
//只要满足是父类就行
System.out.println(e.getMessage());
}
}
}
class Student {
int id;
public Student(int id){
if(id>0){
this.id = id;
}
else{
//抛出异常的目的是:如果有异常就不执行try-catch后面的部分
throw new RuntimeException("id不能为负数");
//因为抛出的是一个对象,所以要new
//一般手动抛出都选择throw new 一个RuntimeException或者Exception
//Exception则需要满足编译器也通过
}
}
@Override
public String toString() {
return "Student [id=" + id + "]";
}
}
因为这里是throw了一个运行时异常,所以构造方法并没有throws RuntimeException
如果是手动throw抛出一个编译时异常对象,那么该方法则必须throws出来
class Student {
int id;
public Student(int id) throws Exception{
//抛出编译时异常则需要满足编译器也语法通过
if(id>0){
this.id = id;
}
else{
throw new Exception();
}
}
@Override
public String toString() {
return "Student [id=" + id + "]";
}
}
throw new Exception()和throw new Exception("id不能为负数")
的区别如下
...throw new Exception();
...
...System.out.println(e.getMessage());
//输出null
...throw new Exception("id不能为负数");
...
...System.out.println(e.getMessage());
//输出id不能为负数
手动throw异常一般都是throw new Exception或者throw new RuntimeException
自定义异常类一般也是只继承与这两种类型
异常是类,所以需要从现有的异常类中继承
创建一个MyException类,继承于RuntimeException
class MyException extends RuntimeException{
//也可以继承Exception,区别就是Exception在抛出的方法中需要再throws
static final long serialVersionUID = -123456789L;
public MyException(){}
public MyException(String msg){
super(msg);
}
}
其他的用法跟普通异常类完全一样,继承与Exception的需要在编译器更明确指出一个throws
一般都是throw谁就throws谁
public Student(int id) throws MyException {
if(id>0){
this.id = id;
}
else{
throw new MyException();
}
}
最后的try-catch可以catch(MyException)也可以catch(Exception)
程序:静态的代码,没有运行起来的才叫程序
eg:文件夹中的所有文件
整个文件夹内包括的所有静态代码的集合就是程序
进程:是程序的一次运行or正在运行的一个程序,是一个动态的过程,并且有自身的产生、存在、消亡的过程——生命周期
eg:运行中的QQ、运行中的LOL、运行中的360安全卫士
线程:进程可以细分为线程,是程序内部的一条执行路径。如果一个进程可以同时并行执行多个线程,就是支持多线程的。
线程是调度和执行的单位,有独立的运行栈和程序计数器,线程切换的开销小
一个进程中多个线程共享相同的资源,因此可以进程间通信更高效,但也带来了安全隐患
eg:同时扫漏洞、扫毒、清理垃圾就是多线程,多线程增加了cpu利用率
eg:图形化界面的基本都是多线程
一个java应用程序java.exe至少有三个线程:main()主线程、gc()垃圾回收线程、异常处理线程
单核CPU:实际上是一种假的多线程,表象的多线程实际上是快速在不同线程之间切换(时分复用),是一种并发
eg:同一个进程,使用单核cpu,单线程比多线程更快,因为没有线程切换时间
多核CPU:可以实现并行
eg:现在的很多手机都是8核心,但这8核并不是完全相同,当手机写备忘录时,调用功耗小功能更弱的核,玩游戏时调用更强但功耗大的核
并发:一个CPU同时执行多个线程
并行:多个CPU同时执行多个线程
java语言JVM允许程序运行多个线程,通过java.lang.Thread类来体现
查API文档
JDK定义了接口Runnable,内含run()方法,这个方法可以做任何事,需要重写
线程Thread类是Runnable的实现
Thread类包含了多个属性和方法
常用方法
- void start(): 启动线程,并执行对象的run()方法
- void run(): 线程在被调度时执行的操作
- String getName(): 返回线程的名称
- void setName(String name):设置该线程名称
- static Thread currentThread(): 返回当前线程。在Thread子类中就 是this,通常用于主线程和Runnable实现类
- static void yield():线程让步。暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程 若队列中没有同优先级的线程,忽略此方法。
- join() :当某个程序执行流中调用其他线程的 join() 方法时,调用线程将 被阻塞,直到 join() 方法加入的 join 线程执行完为止,低优先级的线程也可以获得执行。该方法需要try-catch
- static void sleep(long millis):(指定时间:毫秒) 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。 抛出InterruptedException异常。该方法需要try-catch
- stop(): 强制线程生命期结束,不推荐使用
- boolean isAlive():返回boolean,判断线程是否还活着
线程休眠:sleep
public static void sleep(long millis)throws InterruptedException
设置休眠的毫秒数,一到时间自动唤醒
public static void sleep(long millis,int nanos)throws InterruptedException
设置休眠的毫秒数和纳秒数,一到时间自动唤醒
线程中断:interrupt
public boolean isInterrupted()
//m1.isInterrupted()判断m1是否中断
public void interrupt()
//m1.isterrupt中断m1
线程强制执行:join
Thread类中的join()方法原理
- 一般是用在main方法中,用于占用主线程。
- 副线程必须先start再join才有效
- join方法的本质就是用调用方线程给主线程main上锁直到该线程结束才释放
- 遇到join时,主线程立即被占用,不会存在安全问题,因为本质就是同步机制
- 如果同时存在两个以上的副线程,并且同时对main用join上锁,那么main线程的恢复执行以最后一次锁结束为准,不同线程之间不受影响,因为他们占用的都是main线程而没有彼此占用
class J1Thread extends Thread{
@Override
public void run() {
for(int i=1;i<100;i++){
System.out.println(Thread.currentThread().getName()+"\t11111111111");
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class J2Thread extends Thread{
public void run() {
for(int i=1;i<100;i++){
System.out.println(Thread.currentThread().getName()+"\t22222222222");
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class JoinTest {
public static void main(String[] args) throws InterruptedException {
J1Thread j1 = new J1Thread();//创建线程对象
J2Thread j2 = new J2Thread();//创建线程对象
for(int i =1;i<100;i++) {//主线程main真正开始的地方
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+"\t主线程开始"+i);
}//执行完for循环体才会轮得到后面j1的开始
j1.start();//先创建j1的线程
j1.join();//用j1给主线程main上锁,知道j1结束才释放
for(int i =1;i<100;i++) {//主线程main继续
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+"\t主线程中途"+i);
}//执行完for循环体才会轮得到后面j2的开始
j2.start();
for(int i =1;i<100;i++) {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+"\t主线程结束"+i);
if(i==90)//for循环体途中用join上锁
j2.join();//然后j2占用main线程,把剩余部分执行完毕
}
}
}
线程让步:yield()
本质:多线程并发执行是多个线程交替执行,对于某一个线程并不会执行一下就马上释放,而是会持续一个随机的很短的时间。因此通过yield方法可以强制让正在占用资源的线程让出资源,让包括自己在内的所有线程去争夺资源。因此
- yield没有join那么强硬,join是直接上锁,yield只是放弃资源让大家重新争夺
- yield也没有sleep那么稳定,yield有极小概率没有任何让步效果,而sleep则稳定延迟
- 执行次数足够多时yield也能明显看到让步效果
- 同样的yield无论写多少个都一样,而同样的sleep写10个就有10倍的延迟效果
class MyThread1 implements Runnable{
public void run() {
for (int i = 100; i < 200; i++) {
System.out.println("11111111礼让");
yield();
}
}
}
class YouThread1 implements Runnable{
public void run() {
for (int i = 100; i < 200; i++) {
System.out.println("22222222");
}
}
}
public class YiledTest {
public static void main(String[] args) {
MyThread1 M1 = new MyThread1();
YouThread1 Y1 = new YouThread1();
Thread T1 = new Thread(M1);
Thread T2 = new Thread(Y1);
T1.start();
T2.start();
}
}
- 创建一个Thread的子类MyThread
- 重写run()方法
- new一个Mythread的对象
- 利用对象调用start()方法运行
如果最终是用对象调用run()方法,则没有创建多线程,执行的顺序还是单线程的顺序
例如:
public class ThreadTest {
public static void main(String[] args) {
new MyThread().start();//0号线程(空参构造)
new MyThread().start();//1号线程(空参构造)
new YouThread().start();//2号线程(空参构造)
//如果是 new MyThread().run();
// new YouThread().run();
//那就是先执行上面的全部,然后执行下面的全部
}
}
class MyThread extends Thread {
public void run() {
for (int i = 1; i < 1999; i++) {
if (i % 20 == 0) {
System.out.println(Thread.currentThread().getName()+":"+i);
}else{
try {//sleep方法必须要try-catch异常处理
sleep(1);//暂停线程1ms
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class YouThread extends Thread{
public void run() {
for (int j = 1; j < 100; j++) {
System.out.println(this.getName()+":匿名子类多线程测试");
try {
sleep(20);//暂停线程20ms
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
同一个MyThread类的对象m1只能调用一次start,若想再次调用必须再new一个m2,或者直接匿名对象调用。
在线程类MyThread的定义中,this.相当于Thread.currentThread().
如果创建一个一次性线程,可以造一个匿名子类
new Thread(){ public void run(){ 方法体内容 } }.start();
setName()方法、构造器方法
注意:用构造器重命名需要在Thread子类MyThread中重写构造方法,因为构造方法不继承
class MyThread extends Thread {
public MyThread(String str) {
super(str);
}
public void run() {
MyThread m1 = new MyThread();
m1.setName("重命名xxx");
m1.start();
//先重命名后调用start
或者
MyThread m2 = new MyThread("重命名yyy");
即便是重命名了,每个创建的线程本身的编码private static int threadInitNumber;并不会改变,再次new一个线程,threadInitNumber仍然会在上一个基础上+1,并且与哪类Thread子类无关
例如:
public class ThreadTest {
public static void main(String[] args) {
new MyThread("线程一").start();//threadInitNumber=0
new MyThread("线程二").start();//threadInitNumber=1
new YouThread().start();//threadInitNumber=2
new Thread(){//new一个匿名子类的匿名对象 threadInitNumber=3
public void run(){
for (int i=1;i<2000;i++){
System.out.println(this.getName()+"匿名子类重写run方法");
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
对于主线程,原名main,更名方法如下
...main...{
MyThread m1 = new MyThread();
m1.setName("线程1");
Thread.currentThread().setName("主线程");
for(int i= 1;i<2000;i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
线程优先级有1~10十种等级,其中定义了三个静态常量,最小值,默认值,最大值
优先级可以通过setPriority()方法设置,其中
- setPriority(10)相当于setPriority(MAX_PRIORITY)
- setPriority(1)相当于setPriority(MIN_PRIORITY)
- 不设置相当于setPriority(NORM_PRIORITY)
- 设置完优先级,线程的执行仍然是多线程同时进行,只不过从概率角度讲,同一时间PRIORITY高的线程更容易执行,因此优先级高的线程更容易早执行完。
- 优先级差别越大效果越明显,线程运行内容越多效果越明显(大数定律)
public class PriorityTest {
public static void main(String[] args) {
MyThread m1 = new MyThread("副线程1");
m1.setPriority(Thread.MAX_PRIORITY);
MyThread m2 = new MyThread("副线程2");
m2.setPriority(Thread.MAX_PRIORITY);
m1.setPriority(1);
m2.setPriority(NORM_PRIORITY);
//也可以m2.setPriority(5);
//不设置优先级就默认m2.setPriority(NORM_PRIORITY);
m2.start();
m1.start();
}
}
class MyThread extends Thread{
public MyThread(String name){
super(name);
}
@Override
public void run() {
for(int i=1;i<100;i++){
System.out.println(getName()+":"+i);
}
}
}
- 创建一个实现了Runnable接口的类,这个类可以再派生子类
- 用这个实现类or其子类去实现Runnable中的抽象方法run()
- new这个实现类的对象
- 把这个对象作为参数传给Thread,创建Thread对象
- 经过两次new,用最终的Thread对象调用start()
class MyrunThread implements Runnable {
public void run() {
for (int i = 1; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class RunnableTest {
public static void main(String[] args) {
new Thread(new MyrunThread()).start();
//一步写法
MyrunThread m1 = new MyrunThread();
Thread M1 = new Thread(m1);
//两步写法
M1.start();
}
}
1,开发中优先选择Runnable接口方式,原因如下
- 该方法没有单继承的局限性
- 适合处理多线程共享数据的情况
2,联系:public class Thread implements Runnable
3,相同点:
- 都需要重写run()
- 都要用最终对象调start()而不是调run()
4,区别:
- 继承方式new一次,接口方式new两次
使用Runnable接口可以解决多线程的单继承问题,但是Runnable接口以及其Thread实现中的run方法都没有返回值,Callable接口解决了返回值问题
与Runnable相比,Callable功能更强大
java.lang.Runnable是JDK1.0产物
java.util.concurrent.Callable是JDK5.0产物
- Runnable的run方法 相当于 Callablle的call方法(有返回值): 返回值由Future接口获取
- call()方法可以抛异常: run方法如果出现异常只能try-catch在内部处理,而call方法出现异常可以throws
- call()方法支持泛型的返回值 :可选设置项,避免向下转型带来的安全隐患
- 需要借助FutureTask类,比如获取返回结果
java.util.concurrent.Callable 下 Callable 接口的定义
@FunctionalInterface
public interface Callable{
public V call() throws Exception;
}
class Mythread implements Callable<泛型类X>{ public 返回值类型(类名X) call () throws 错误类型 { 方法体 return 能转成X的对象 } } public class Test{ public static void main(String[] args){ Mythread M1 = new Mythread(); FutureTask F1 = new FutureTask(M1);//和Runnable接口相比,多了这一步 Thread T1 = new Thread(F1,"线程名"); T1.start();//start运行call方法,但是不一定必须接收返回值 System.out.println(F1.get()); } }
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class Mythread implements Callable{
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1 ; i<100 ; i++){
if(i%4==0){
System.out.println(i);
sum+=i;
}
}
return sum;
}
}
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Mythread M1 = new Mythread();//实现Callable接口实例
FutureTask F1 = new FutureTask(M1);//传入FutureTask中
Thread T1 = new Thread(F1);//传入Thread中
T1.start();
System.out.println(F1.get());
//start运行call方法,但是不一定必须接收返回值
}
}
开发中一般都是用线程池
如果创建的多个线程将对同一个共享数据进行操作,就可能导致读入和改写数据的各种错误。为了避免冲突,不同线程在对共享数据进行操作的时候应该有一个同步机制,甲访问该共享数据的时候应该对其上锁,乙丙丁等线程在上锁处停滞,等待甲操作完成再重复该步骤
当多个线程同时操作同一资源时,对资源的保护正确读写的操作,有synchronized代码块、synchronized方法
以卖3个窗口100张票为例
该方法尽量使用Runnable接口方式来实现
格式:
synchronized(同步监视器){
//同步监视器可以是任意类的对象,同一个对象同一把锁,处于同一个同步机制里
需要被同步的代码体
}
class MyThread implements Runnable {
private static int ticket = 100;
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "窗口准备卖票");
if (ticket == 0) {
System.out.println("已售罄");
break;
}
synchronized (this) {
System.out.println(Thread.currentThread().getName()+"售票一张,余票:"+ --ticket);
}
}
}
}
public class SynchronizedTest {
public static void main(String[] args) {
Thread m = new Thread(new MyThread());
m.start();
}
}
如果改用extends方式来实现就会出现问题,因为synchronized同步代码块中的同步监视器(锁)是this,即为当前对象,而extends方法需要调用3个对象,所以上了3把锁,此时跟没上锁的效果是一样的,很容易就同一张票卖了几次
class MyThread extends Thread{
private static int ticket = 100;
public MyThread(String name){
super(name);
}
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "窗口准备卖票");
if (ticket == 0) {
System.out.println("已售罄");
break;
}
synchronized (this) {
System.out.println(Thread.currentThread().getName()+"售票一张,余票:"+ --ticket);
}
}
}
}
public class SynchronizedTest{
public static void main(String[] args) {
MyThread m1 = new MyThread("窗口1");
m1.start();
MyThread m2 = new MyThread("窗口2");
m2.start();
MyThread m3 = new MyThread("窗口3");
m3.start();
}
}
改进后的extends方式就是直接声明一个静态的任意对象当作同步监视器
private static Object obj = new Object();
class MyThread extends Thread{
private static int ticket = 100;
private static Object obj = new Object();
public MyThread(String name){
super(name);
}
public void run() {
while (true) {
if(ticket<10) {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// try {
// sleep(10);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "窗口准备卖票");
if (ticket == 0) {
System.out.println("已售罄");
break;
}
System.out.println(Thread.currentThread().getName()+"售票一张,余票:"+ --ticket);
}
}
}
}
此时的同步代码块包括了
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "窗口准备卖票");
if (ticket == 0) {
System.out.println("已售罄");
break;
}
如果把同步代码块缩小
System.out.println(Thread.currentThread().getName() + "窗口准备卖票");
if (ticket == 0) {
System.out.println("已售罄");
break;
}
synchronized (obj) {
System.out.println(Thread.currentThread().getName()+"售票一张,余票:"+ --ticket);
}
}
运行后
原因:3个线程只要有一个已经经过了if判断,但卡在了synchronized之前,那么他就会导致ticket=0变成ticket=-1,此后再也不会经过if语句来break了,所以停不下来
if (ticket == 0) {
System.out.println("已售罄");
break;
}
但如果同步代码块框的太多,跟单线程没区别
public 返回值 synchronized 方法(){ } 相当于 public 返回值 方法(){ synchronized(this){ .................... } //同步方法相当于把整个方法用同步代码块框起来 }//并且以this为同步监视器
一般都是定义一个synchronized方法,把这个方法放在run里
效果和注意点跟同步代码块相同
public void run(){
System.out.println(Thread.currentThread().getName() + "窗口准备卖票");
show();
}
private synchronized void show(){
while(true){
if (ticket == 0) {
System.out.println("已售罄");
break;
}
System.out.println(Thread.currentThread().getName()+"售票一张,余票:"+ --ticket);
}
}
}
不同的线程分别需要占用对方需要的同步资源不放(不释放锁),都在等对方先放弃自己所需要的同步资源(锁),就形成的线程的死锁
出现死锁之后不会报错,也不会出现提示,所有线程都处于阻塞状态,无法继续
解决死锁的方法:
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
一个线程run中A锁套B锁,另一个线程run中B锁套A锁,一旦两个同时运行就容易出现死锁。尤其是设置一个sleep沉睡,死锁更容易出现
class Mythread implements Runnable{
public void run(){
......
synchronized(锁A){
//A锁套B锁
synchronized(锁B){
......
}
}
......
}
}
class Youthread implements Runnable{
public void run(){
......
synchronized(锁B){
//B锁套A锁
synchronized(锁A){
......
}
}
......
}
}
//主方法体中
new Thread(new Mythread()).start();
new Thread(new Youthread()).start();
ReentrantLock类的实例化有一个形参fair
private ReentrantLock lockkk = new ReentrantLock(true);叫公平锁
private ReentrantLock lockkk = new ReentrantLock();和
private ReentrantLock lockkk = new ReentrantLock(false);叫非公平锁
公平锁:多个共用同一把锁的线程有序排队,并按排队顺序上锁
private ReentrantLock lockkk = new ReentrantLock(true);//公平锁
。。。。。。。。。
public void run(){
for(int i = 1;i<=100;i++){
lockkk.lock();//上公平锁lockkk
System.out.print(Thread.currentThread().getName());
Depot(this.account,1000);
//输出内容
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}//加上sleep效果更明显,防止因为屏幕输出时间差造成的误导
lockkk.unlock();//释放公平锁lockkk
//因为是公平锁,被阻塞的线程按顺序排队进入被lock和unlock方法夹在中间的代码块,
//此后该调用lock的线程进入排队的末尾,不与其他线程争抢资源
//配合上一个不短的sleep放大有序排队的效果,如果sleep值过小仍然可能冲突
}
}
非公平锁:多个共用同一把锁的线程有序排队,但不按顺序上锁,仍像synchronized一样靠抢
private ReentrantLock lockkk = new ReentrantLock(false);
private ReentrantLock lockkk = new ReentrantLock();
//两种表达都是表示非公平锁
public void run(){
for(int i = 1;i<=100;i++){
lockkk.lock();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}//sleep也可以放在输出语句之前,只要是在lock和unlock之间就行了
System.out.print(Thread.currentThread().getName());
Depot(this.account,1000);
lockkk.unlock();
}
}
上面的ReentrantLock类提供的lock公平锁可以实现多个线程按顺序每个都执行一次,当
线程数=2时,wait notify方法也能达到这样的效果
private static Object obj = new Object();
public void run(){
synchronized(obj)
for(int i = 1;i<=100;i++){
obj.notify();//唤醒另一个线程
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(Thread.currentThread().getName());
Depot(this.account,1000);
try {
obj.wait();//本线程阻塞,并释放同步监视器obj
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
当线程数>2时,即便是用notifyAll()也不能达到按顺序的效果,因为此时唤醒了其他线程之后,不像lock公平锁那样按顺序排队,notifyAll只管唤醒,线程资源的争夺仍然是随机的
- wait():阻塞自己,释放锁(同步监视器)。wait之后仍然可以有语句
- notify():唤醒一个线程,按优先级随机。一般放在代码块最开头
- notifyAll():唤醒其他所有线程,不按顺序
wait()、notify()、notifyAll():三个方法都是
- 写在同步方法or同步代码块中
- 需要用同步监视器(锁对象)来调用
- 因此定义在java.lang.Object中,因为需要所有对象都能调用
- 报错信息是IllegalMonitorStateException
wait会释放同步监视器,其他线程可以进入synchronized代码块内部并获取锁
sleep不会释放同步监视器,如果是同步线程,那么sleep会导致整个多线程阻塞
wait定义在Object类中,需要在同步代码块中用同步监视器对象调用,sleep是定义在Thread类中的静态方法,随时可以调用
https://blog.csdn.net/m0_56079407/article/details/119790045?spm=1001.2014.3001.5501
https://blog.csdn.net/m0_56079407/article/details/119839272?spm=1001.2014.3001.5501
根据当前时间or根据()内指定的时区创建对象
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
//部分结构省略
LocalDate ld = LocalDate.now();
LocalTime lt = LocalTime.now();
LocalDateTime ldt = LocalDateTime.now();
System.out.println(ld); //2021-08-24
System.out.println(lt.toString()); //22:24:42.334
System.out.println(ldt); //2021-08-24T22:24:42.334
例如LocalDateTime的of方法可以在形参放 年,月,日,时,分,秒,纳秒
LocalDateTime ldt = LocalDateTime.of(1997,7,17,5,20,13,14);
System.out.println(ldt.toString());//1997-07-17T05:20:13.000000014
这是一个非static方法,所以需要由对象来调用,常用now和of
System.out.println(LocalDateTime.now().getYear());
System.out.println(LocalDateTime.now().getMonth());
System.out.println(LocalDateTime.now().getDayOfMonth());
System.out.println("******************************");
System.out.println(LocalDate.of(1997,7,17).getYear());
System.out.println(LocalDate.of(1997,7,17).getMonthValue());
System.out.println(LocalDate.of(1997,7,17).getDayOfMonth());
System.out.println("******************************");
System.out.println("今天是:"+LocalDateTime.now().getDayOfWeek());
System.out.println("今天是星期:"+LocalDateTime.now().getDayOfWeek().getValue());
System.out.println("今天是今年的第__天:"+LocalDateTime.now().getDayOfYear());
with方法也是一个非static方法,需要用对象去调用,可调用的方法如图
of方法就单纯的只能设置指定的日期时间,而with可以设置本年、本月的第xx天
非static方法,需要由对象调用,可调用的方法如图
now和ofEpochMilli是静态方法,由类直接调用
toEpochMilli由对象调用
Instant ins = Instant.now();//获取本初子午线的标准时间
System.out.println(ins.toEpochMilli());//返回的是一个long型数
//toEpochMilli:获取自1970-1-1 00:00:00(UTC)开始的毫秒数
Instant tant = Instant.ofEpochMilli(1529821917736L);//给定毫秒数,获取Instant实例
System.out.println(tant);//2018-06-24T06:31:57.736Z
格式:对象.atOffset(ZoneOffset. ofxxx)如图:设置距离本初子午线的时间偏移量
返回值:返回一个OffsetDateTime的对象,输出该对象相当于输出LocalDateTime再拼接一个时区偏移量
直接输出这个对象相当于输出他的toString方法
OffsetDateTime osdt = Instant.now().atOffset(ZoneOffset.ofHours(-8));就是设置西八区OffsetDateTime osdt = Instant.now().atOffset(ZoneOffset.ofHours(8));就是设置东八区
测试:
System.out.println(LocalDateTime.now());//用localdatetime做一下区别
System.out.println(Instant.now());
OffsetDateTime osdt = Instant.now().atOffset(ZoneOffset.ofHours(8));//我国东八区时间
OffsetDateTime osdt111 = Instant.now().atOffset(ZoneOffset.ofHoursMinutesSeconds(-8,-10,-30));
//相比本初子午线西偏(减去)8h10m30s,注意必须全部正or全部负
System.out.println(osdt);
System.out.println(osdt111);
//调用toString方法
格式化format:把指定的兼容的日期按DateTimeFormat中的格式化方法转化为字符串从而输出
解析parse:把指定格式的日期字符串赋值给parse方法,变成指定的日期形式
格式化方法有三种
预定义的标准格式。如: ISO_LOCAL_DATE_TIME;ISO_LOCAL_DATE;ISO_LOCAL_TIME
本地化相关的格式。如:ofLocalizedDateTime(FormatStyle.LONG)、ofLocalizedDateTime(FormatStyle.MEDIUM)、ofLocalizedDateTime(FormatStyle.SHORT)、ofLocalizedDateTime(FormatStyle.FULL)
自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”)
其中最常用的是自定义的格式
java.time.format.DateTimeFormatter类中提供了
三个设置格式方法:用于控制输出日期的格式
提供了format格式化方法:用于把local日期对象转为上面设置的格式
解析parse方法:则是把字符串日期设置为日期对象,从而可以调用日期对象可以调用的方法
1,预定义格式:用的少,可读性不高
// 预定义的标准格式。如:
// ISO_LOCAL_DATE_TIME;
// ISO_LOCAL_DATE;
// ISO_LOCAL_TIME
//1.format格式化:日期——>字符串 ——>输出
LocalDateTime ldt = LocalDateTime.now();//获取时间
DateTimeFormatter dtf = DateTimeFormatter.ISO_LOCAL_DATE_TIME;//本地化
//ISO_LOCAL_DATE_TIME方法返回一个DateTimeFormatter对象
System.out.println(ldt);
System.out.println(dtf);
System.out.println(dtf.format(ldt));//先本地化然后格式化输出
//格式化日期之后再输出才是正确是字符串格式
//2.parse解析: 字符串——>日期
System.out.println(dtf.parse(ldt.toString()));
System.out.println(dtf.parse("2021-08-25T14:54:38.403"));
//相当于调用toString方法
// LocalDate ld = LocalDate.now();
// System.out.println(dtf.parse(ld.toString()));
// 会报错
2,本地化相关格式,国语话,可读性不错
//本地化相关的格式。如:ofLocalizedDateTime(FormatStyle.LONG)、//ofLocalizedDateTime(FormatStyle.MEDIUM)、
//ofLocalizedDateTime(FormatStyle.SHORT)、
//ofLocalizedDateTime(FormatStyle.FULL)
LocalDateTime ldt = LocalDateTime.now();//获取本地日期时间对象
DateTimeFormatter dtf1 = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT);//本地化
DateTimeFormatter dtf2 = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM);//本地化
DateTimeFormatter dtf3 = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG);//本地化
System.out.println(dtf1.format(ldt));//格式化并输出
System.out.println(dtf2.format(ldt));//格式化并输出
System.out.println(dtf3.format(ldt));//格式化并输出
LocalDate ld = LocalDate.now();
DateTimeFormatter dtf4 = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL);
//这里是ofLocalizedDate而不是ofLocalizedDateTime
System.out.println(dtf4.format(ld).toString());//toString都是可以省略的
3,自定义的格式:最常用:ofPattern方法
其中y代表年、M代表月、d代表日、h代表时、m代表分、s代表秒:一一对应,假设格式为yyyy-yyyy-yyyy则输出的是2021-2021-2021
中间可以添加任意符号,任意设置格式。但最常用的是下面的第三种
DateTimeFormatter dtfm1 = DateTimeFormatter.ofPattern("yyyy-MM-dd **** hh:mm:ss");
System.out.println(dtfm1.format(LocalDateTime.now()));
DateTimeFormatter dtfm2 = DateTimeFormatter.ofPattern("yyyy-MMMM-d h:m:s");
System.out.println(dtfm2.format(LocalDateTime.now()));
DateTimeFormatter dtfm3 = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
System.out.println(dtfm3.format(LocalDateTime.now()));
上面的with plus minus方法都不改变原对象,而是返回一个新创建的对象
一般日期都是用LocalDateTime那三个类
要进行时区or偏移的操作则用Instant类
要自己设置格式则用DateTimeFormat类