Java基础
基本数据类型的取值范围:
类型描述 | 关键字 | 字节数 | 取值范围 |
---|---|---|---|
字节型 | byte | 1 | -27~27-1(-128~127) |
短整型 | short | 2 | -215~215-1(-32768~32767) |
整型 | int | 4 | -231~231-1(-2147483648~2147483647) |
长整型 | long | 8 | -263~263-1 |
单精度浮点型 | float | 4 | 有效位数6~7位 |
双精度浮点型 | double | 8 | 有效位数15位 |
字符型 | char | 2 | 0~216-1(0~65535) |
布尔型 | boolean | 1 | true/false |
数据类型转换规则:
1、八种基本数据类型当中除了boolean之外剩下的7种类型之间都可以互相转换
2、小容量向大容量转换,称为自动类型转换,容量从小到大排序:
byte < short = char < int < long < float < double
注意:任何浮点类型不管占用多少个字节,都比整型容量大。char和short可表示的种类数量相同,但是char可以去更大的正整数
3、大容量转换成小容量,叫做强制类型转换,需要加强制类型转换符,程序才能编译通过,但是在运行阶段可能会损失精度,所以谨慎使用
4、当正数字面值没有超出byte,short,char的取值范围,可以直接赋值给byte,short,char类型的变量
5、byte,short,char混合运算的时候,各自先转换成int类型再做运算
6、多种数据类型混合运算,先转换成容量最大的那种类型再做运算
运算符:
算术运算符:
+,-,*,/,%,++,–
关系运算符:
,>,<,>=,<=,==,!=
逻辑运算符:
&(逻辑与),|(逻辑或),!(逻辑非),^(逻辑异或),&&(短路与),||(短路或)
逻辑与(&)和短路与(&&),逻辑或(|)和短路或(||)的区别:
相同点:
不管是逻辑与(&),还是短路与(&&),最后的与运算结果都是一样的。
区别:
(1)逻辑与(&)运算符,也叫按位与运算符。做运算的时候逻辑与(按位与)两边的表达式都会先计算,然后再的出两边运算的结果。
例:表达式1&表示2,先分别计算表达式1和表达式2的真假,然后再得出结果的真假。
(2)短路与(&&)运算符,主要是可以短路与运算,以减少后面无意义的运算。
例:表示1 && 表达2,从左往后计算每个表达式的真假,如果表达式1为false了的话,与运算结果就为false了,而不用再去计算表达式2的真假,也就是表达式2被短路掉了。
(3)逻辑与(&)和逻辑或(|)还有按位与和按位或的功能,可以做位运算,当这两个运算符两边的为数字时,则进行与/或运算。
赋值运算符:
=,+=,-=,*=,/=,%=
-
赋值类的运算符优先级:先执行等号右边的表达式,将执行结果赋值给左边的变量
-
注意:
byte b = 10; b += 5;//等同于 b = (byte)(b+5); int i = 10; i += 5;//等同于 i = (int)(i+5); long x = 10L; int y = 20; y += x;//等同于 y = (int)(y+x);
-
结论:扩展类的赋值运算符不改变运算结果类型,假设最初这个变量的类型时byte类型,无论怎么进行追加或追减,最终该变量的数据类型还是byte类型。
三目运算符/条件运算符/三元运算符:
? :
string sex = 逻辑值?字面值1:字面值2;//判断逻辑值如果为true,将字面值1赋给sex,否则将字面值2赋给sex
控制语句:
switch:
switch后面和case后面只能是int或者String类型的数据,不能是探测其他类型。
- 当然byte,short,char也可以直接写到switch和case后面,因为他们可以进行自动类型转换。
- JDK 6的switch和case后面只能探测int类型。
- JDK 7之后包括7版本在内,引入新特性,switch关键字和case关键字后面可以探测int或String类型的数据以及枚举类型(enum)
switch语句case合并,满足合并后的任意一个case即可执行语句下的语句
int i = 1;
switch(i){
case 1: case 2: case 3:
System.out.println(1);
}
循环:
for,while,do…while
break终止循环:
给for循环起名字后break可以选择终止选定的for循环,while循环同理
A:for(int i = 0; i < 10; i++) {
B:for(int j = 0; j < 10; j++) {
if(i == 5) {
break A;
}
}
}
continue同break用法相同
方法:
方法分类:
没有static关键字的方法称为实例方法,如何访问:“引用.”
方法调用:
1、方法的修饰符列表当中有static关键字,完整的调用方式是:
类名.方法名(实参列表);
2、方法1和方法2在同一个类体当中的时候,“类名.”可以省略不写
3、方法的修饰符列表当中没有static关键字的调用方式是:
引用.方法名(实参列表);
4、当一个行为/动作执行的过程当中是需要对象参与的,那么这个方法一定要定义为实例方法,不要带static关键字
方法执行过程中 JVM 的内存分配:
1、方法只定义,不调用,是不会执行的,并且在JVM中也不会给该方法分配“运行所属”的内存空间,只有在调用这个方法的时候,才会动态的给这个方法分配所属的内存空间
2、在JVM内存划分上有三块主要的内存空间(还有其他内存空间):
- 方法区内存:在类加载的时候,class字节码代码片段被加载到该内存空间
- 堆内存:new的对象在堆内存中存储,对象内部有实例变量,所以实例变量存在堆内存中
- 栈内存:一条线程分配一个栈内存空间,方法代码片段执行的时候,会给该方法分配内存空间,在栈内存中入栈
3、方法代码片段存在哪里?方法执行的时候执行过程的内存在哪里分配?
- 方法代码片段属于 .class 字节码文件的一部分,字节码文件在类加载的时候,将其放到了方法区当中,所以jVM中的三块主要内存空间中方法区内存最先有数据,存放了代码片段。
- 代码片段虽然在方法内存当中只有一份,但是可以被重复调用,每一次调用这个方法的时候,需要给该方法分配独立的活动场所,在栈内存中分配。(栈内存中分配方法运行的所属内存空间)
4、方法在调用的瞬间,会给该方法分配内存空间,会在栈中发生入栈动作,方法执行结束之后,给该方法分配的内存空间全部释放,此时发生出栈动作。
5、局部变量在“方法体”中声明,局部变量运行阶段,内存在栈中分配。
6、变量分类:
- 局部变量(方法体中声明)
- 成员变量(方法体外、类体中声明)
-
实例变量(前边修饰符没有static)
实例变量属于对象级别的变量,这种变量必须先有对象才能有实例变量。实例变量没有手动赋值的时候,系统默认赋值,在类加载的时候只加载了代码片段,还没有创建对象,所以此时实例变量并没有初始化。实际上,实例变量的内存空间实在构造方法执行过程中完成开辟的,也就是初始化,系统在默认赋值的时候,也是在构造方法执行过程当中完成的赋值。
-
静态变量(前边修饰符中有static)
-
7、静态变量存储在方法区内存当中
8、三块内存当中变化最频繁的是栈内存,最先有数据的是方法区内存,垃圾回收器最重要针对的是堆内存
9、垃圾回收器(自动垃圾回收机制、GC机制)什么时候会考虑将某个Java对象的内存回收?
- 当堆内存中的Java对象称为垃圾数据的时候,会被垃圾回收器回收
- 什么时候堆内存中的Java对象会成为垃圾?
- 没有更多引用指向它的时候
- 这个对象无法被访问,因为访问对象只能通过引用的方式访问
总结:在代码执行阶段,类加载器(ClassLoader)会在硬盘上找到 .class 文件,并将它装载到 JVM 的方法区内存中,在执行方法的过程中每调用一个方法都会发生入栈动作,将调用的方法装载到栈内存中,当方法执行结束后发生出栈动作,当栈为空时,即代表整个程序执行阶段的结束。
方法执行的内存分析:
方法调用的时候,在参数传递的时候,实际上传递的是变量中保存的“值”,而不是原方法中变量的地址。
栈内存中存放的是局部变量。
方法重载:
- 方法重载也被称为 overload
- 什么时候考虑使用方法重载?
- 功能相似的时候,尽可能让方法名相同
- 什么条件满足之后构成方法重载?
- 在同一个类中
- 方法名相同
- 参数列表不同:
- 数量不同
- 顺序不同
- 类型不同
- 方法重载和什么有关系,和什么没有关系?
- 和方法名、参数列表有关
- 和返回值类型无关
- 和修饰符列表无关
面向对象:
面向对象和面向过程的区别:
面向过程:主要关注点是实现的具体过程,因果关系
- 优点:对于业务逻辑比较简单的程序,可以达到快速开发,前期投入成本较低。
- 缺点:采用面向过程的方式开发很难解决非常复杂的业务逻辑,另外面向过程的方式导致软件元素之间的“耦合度”非常高,只要其中一环出问题,整个系统受到影响;导致最终的软件“扩展力”差。另外,由于没有独立体的概念,所以无法达到组件复用。
面向对象:主要关注点是对象(独立体)能完成哪些功能
- 优点:耦合度低,扩展力强。更容易解决显示世界当中更复杂的业务逻辑,组件复用性强。
- 缺点:前期投入成本较高,需要进行独立体的抽取,大量的系统分析与设计。
C语言是纯面向过程的、C++是半面向对象、Java纯面向对象
面向对象更符合人的思维方式
面向对象的三大特征:封装、继承、多态
采用面向对象的方式开发一个软件,生命周期中(整个生命周期中贯穿使用OO面向对象方式):
- 面向对象的分析:OOA
- 面向对象的设计:OOD
- 面向对象的编程:OOP
Java中的两种数据类型:
基本数据类型
引用数据类型:
String、System、、、等(由sun公司提供的)
其他的 .class 文件(由程序员定义的class)
java语言中所有的class都属于引用数据类型
类:
通过一个类可以实例化N个对象
实例化对象的语法:new 类名();
new是java语言当中的一个运算符,new运算符的作用是创建对象,在JVM堆内存当中开辟新的内存空间。
例:
public static void main(String[] args) {
Student s = new Student();
}
-
s是一个变量名,一个局部变量(存储在栈内存中),像这个例子中的 “s” 的统称为“引用”
-
什么是对象?new运算符在堆内存中开辟的内存空间称为对象
-
什么是引用?引用是一个变量,只不过这个变量中保存了另一个java对象的内存地址
-
在java语言中,只能通过“引用”去访问堆内存中对象内部的实例变量,访问格式:
读取数据:引用.变量名
修改数据:引用.变量名 = 值
java.NullPointerException:空引用访问“实例”相关的数据一定会出现空指针异常
“实例”相关的数据:这个数据访问的时候必须有对象的参与,这种数据就是“实例”相关的数据
封装:
为什么要封装?封装有什么好处?
封装的好处:
- 封装之后,对于那个事物来说,看不到这个事物比较复杂的那一面,只能看到该事物简单的那一面。复杂性封装,对外提供简单的操作入口。照相机就是一个很好的封装的案例,照相机的实现原理非常复杂,但是对于使用照相机的人来说,操作起来是非常方便的是非常便捷的。还有像电视机也是封装的,电视机内存实现非常复杂,但是对于使用者来说不需要关心内部的实现原理,只需要会操作遥控器就行。
- 封装之后才会形成真正的"对象”,真正的“独立体"
- 封装就意味着以后的程序可以重复使用,并且这个事物应该适应性比较强,在任何场合都可以使用。
- 封装之后,对于事物本身,提高了安全性。【安全级别高】
封装的步骤:
1、所有属性私有化,使用private关键字进行修饰,private表示私有的,修饰的所有数据只能在本类中访问。
2、对外提供简单的操作入口,也就是说以后外部程序要想访问age属性,必须通过这些简单的入口进行访问:
- 对外提供两个公开的方法,分别是set方法和get方法
- 想要修改某个属性,调用set方法
- 想要读取某个属性,调用get方法
3、set方法的命名规范:
public void setAge(int a) {//set+属性名首字母大写
//可以在这里填写安全控制代码,比如:进行if判断等
age = a;
}
4、get方法命名规范:
public void getAge() {//get+属性名首字母大写
return age;
}
注意:
- setter和getter方法没有static关键字
- 有static关键字修饰的方法怎么调用:类名.方法名(实参);
- 没有static关键字修饰的方法怎么调用:引用.方法名(实参);
构造方法:
1、构造方法又被称为构造函数/构造器/Constructor
2、构造方法语法结构:
【修饰符列表】 构造方法名(形参列表){
方法体;
}
3、对于构造方法来说,不需要指定返回值类型,并且也不能写void
4、构造方法名和它的所在类名一致
5、构造方法的作用:
- 构造方法的的意义是,通过构造方法的调用,可以创建对象
- 创建对象的同时初始化实例变量的内存空间
6、构造方法的调用:new 构造方法名(实参列表);
7、构造方法调用执行之后,有返回值吗?
每一个构造方法实际上执行结束之后都有返回值,但是这个return语句不需要写,构造方法结束的时候java程序自动返回值,并且返回值类型是构造方法所在类的类型,由于构造方法的返回值类型就是类本身,所以返回值类型不需要编写。
8、当类中没有定义任何构造方法的话,系统默认给该类提供一个无参数的构造方法,这个构造方法被称为缺省构造器
9、当一个类显示的将构造方法定义出来了,那么系统则不再为这个类提供缺省构造器,建议开发中手动的为当前类提供无参数的构造方法,因为无参数构造方法很常用
10、构造方法支持重载机制
参数传递:
java语言中方法调用的时候涉及到参数传递的问题,参数传递实际上传递的是变量中保存的具体值。
结论:方法调用的时候,涉及到参数传递的问题,传递的时候,java只遵循一种语法机制,就是将变量中保存的“值”传递过去了,只不过有的时候这个值是一个字面量,有的时候这个是一个java对象的内存地址
java中的String虽然是对象,但是具有值类型的特征。他是值传递,传递的是值
this关键字:
1、this是一个引用,this是一个变量,this变量中保存了内存地址指向了自身,this存储在JVM堆内存java对象内部
2、创建100个对象,每一个对象都有this,也就是说有100个不同的this
3、this可以出现在”实例方法“中,指向当前正在执行这个动作的对象
4、多数情况下”this.“可以省略不写
5、用来区分局部变量和实例变量的时候,“ this. ”不能省略
**注意:**在带有static的方法当中不能直接访问实例变量和实例方法,因为实例变量和实力方法都需要对象的存在,而static的方法当中是么有this的,也就是说当前对象是不存在的,自然也就无法访问当前对象的实例变量和实例方法
封装类中调用构造方法:
//this(实参列表);
//通过这种方式不会创建新的java对象,但同时又可以达到调用其他的构造方法的目的,并且这个语句只能出现在构造方法的第一行
this可以用在哪里?
1、可以用在实例方法中,代表当前对象
2、可以使用在构造方法中,通过当前的构造方法调用其他的构造方法
/*
带有static的方法,其实及可以采用类名的方式访问,也可以采用引用的方式访问
但是即使采用引用的方式访问,实际上执行的时候和引用指向的对象无关,建议还是采用 类名. 的方式访问
比如以下程序是正确的,并不会出现空指针异常
*/
public static void main(String[] args) {
Test t = new Test();
t.doSome();
t = null;
t.doSome();
}
public static void doSome() {
System.out.println("do some!");
}
static关键字:
什么时候成员变量声明为实例变量?
所有对象都有这个属性,但是这个属性的值会随着对象的变化而变化
什么时候成员变量声明为静态变量?
所有对象都有这个属性,并且所有对象的这个属性的值是一样的,建议定义为静态变量,节省内存的开销
关于static关键字:
1、static修饰的方法是静态方法
2、static修饰的变量是静态变量
3、所有static修饰的元素都称为静态的,都可以使用“类名.”的方式访问,也可以用“引用.”的方式访问(但不建议)
static静态代码块:
1、语法格式:
static {
//java语句
}
2、静态代码块在类加载时执行,并且只执行一次
3、静态代码块在一个类中可以编写多个,并且遵循自上而下的顺序一次执行
4、静态代码块的作用是什么?怎么用?用在哪?什么时候用?
和具体的需求有关,例如项目中要求在类加载的时刻,执行代码完成日志的记录,那么这段记录日志的代码就可以编写到静态代码块中,完成日志记录
5、通常在静态代码块中完成预备工作,先完成数据的准备工具,例如:初始化连接池,解析XML配置文件
方法什么时候定义为静态的?
方法描述的是动作,当所有的对象执行这个动作的时候,最终产生的影响是一样的,那么这个动作已经不再属于某一个对象的动作了,可以将这个动作提升为类级别的动作,模板级别的动作
静态方法中无法直接访问实例变量和实例方法
大多数方法都定义为实例方法,一半一个行为或者一个动作在发生的时候,都需要对象的参与,但是也有例外,例如:大多数“工具类”中的方法都是静态方法,因为工具类就是方便变成,为了方便方法的调用,自然不需要new对象是最好的
继承:
1、继承“基本”的作用:代码复用,最“重要”的作用:有了继承才有以后“方法的覆盖”和“多态机制”
2、语法格式:
[修饰符列表] class 类名 extends 父类名 {
类体 = 属性 + 方法
}
3、java语言当中的继承只支持单继承,一个类不能同时继承很多类,只能继承一个类
4、继承中的一些术语:
B类继承A类,其中:
A类称为:父类、基类、超类、superclass
B类称为:子类、派生类、subclass
5、在java语言中子类继承父类可以继承哪些数据?
- 私有的不支持继承
- 构造方法不支持继承
- 其他数据都可以继承
6、虽然java语言中只支持单继承,但是一个类也可以间接继承其他类,例如:
C extends B{
}
B extends A{
}
A extends T{
}
//C直接继承B类,但是C类间接继承A、T类
7、java语言中假设一个类没有显示的继承任何类,该类默认继承JavaSE库当中提供的java.lang.object类,java语言中任何一个类中都有object类的特征
方法覆盖:
1、方法覆盖又被称为方法重写,英语单词:override[官方]/overwrite
2、什么时候使用方法重写?
- 当父类中的方法已经无法满足当前子类的业务需求
- 子类有必要将父类中继承过来的方法进行重新编写
- 这个重新编写的过程称为方法重写/方法覆盖
3、什么条件满足之后方法会发生重写?
- 方法重写发生在具有继承关系的父类和子类之间
- 方法重写的时候:返回值类型相同,方法名相同,形参列表相同
- 方法重写的时候:访问权限不能更低,可以更高
- 方法重写的时候:抛出异常不能更多,可以更少
4、建议方法重写的时候尽量复制粘贴,不要编写,容易出错,导致没有产生覆盖
5、注意:
- 私有方法不能继承,所以不能覆盖
- 构造方法不能继承,所以不能覆盖
- 静态方法不存在覆盖
- 覆盖只针对方法,不针对属性
多态:
1、多态中涉及到的几个概念:
-
向上转型(upcasting)
子类型 --> 父类型
-
向下转型(downcasting)
父类型 --> 子类型
-
需要记忆:
无论是向上转型还是向下转型,两种类型之间必须要有继承关系,没有继承关系,程序是无法通过的
多态的语法机制:
1、Animal和Cat之间存在继承关系,Animal是父类,Cat是子类
2、
Animal a2 = new Cat();
3、new Cat() 创建的对象类型是Cat,a2这个引用的数据类型是Animal,可见它们进行了类型转换
4、java中允许这种语法:父类型引用指向子类型对象
分析:
1、java程序永远都分为编译阶段和运行阶段
2、先分析编译阶段,再分析运行阶段,编译无法通过,根本是无法运行的
3、编译阶段编译器检查这个引用的数据类型为Animal,由于Animal.class,字节码当中有move()方法,所以编译通过了,这个过程称为静态绑定,编译阶段绑定。只有静态绑定成功之后才有后续的运行
4、在程序运行阶段,JVM堆内存当中真实创建的对象是Cat对像,那么以下程序在运行阶段一定会调用Cat对象的move()方法,此时发生了程序的动态绑定,运行阶段绑定
5、无论是Cat类有没有重写move方法,运行阶段一定调用的是Cat对象的move方法,因为第层真实对象就是Cat对象
6、父类型引用指向子类型对象这种机制导致程序存在编译阶段绑定和运行阶段绑定两种不同的形态/状态,这种机制可以成为一种多态语法机制
向下转型问题:
例子中的类之间的关系:Animal是父类,Cat、Bird是子类,都继承了Animal类,但两者间没有继承关系
1、以下程序编译是没有问题的,因为编译器检查到a3的数据类型是Animal,Animal和Cat之间存在继承关系,并且Animal是父类型,Cat是子类型,父类型转换成子类型叫做向下转型,语法合格
2、程序虽然编译通过了,但是程序在运行阶段会出现异常,因为JVM堆内存当中真实存在对象是Bird类型,Bird对象无法转换成Cat对象,因为两种类型之间不存在任何继承关系,此时出现了著名的异常:
java.lang.ClasssCastException
类型转换异常,这种异常总是在“向下转型”的时候会发生
Animal a3 = new Bird();
Cat c3 = (Cat)a3
结论:
1、以上异常只有在强制类型转换的时候会发生,也就是说“向下转型”存在隐患(虽然编译通过,但是运行报错)
2、向上转型只要编译通过,运行一定不会出现问题
3、向下转型编译通过,运行可能出现错误
4、怎么避免向下转型出现的ClassCastException?
使用 instanceof 运算符可以避免出现以上的异常
5、instanceof 运算符怎么使用?
5.1、语法格式:
(引用 instanceof 数据类型名)
5.2、以上运算符的执行结果类型是布尔类型,结果可能是true/false
5.3、关于运算结果true/false
假设:(a instanceof Animal)
true表示:
a这个引用指向的对象是一个Animal类型
flase表示:
a这个引用指向的对象不是一个Animal类型
if(a3 instanceof Cat) {
Cat c3 = (Cat)a3;
}else if(a3 instanceof Bird){
Bird b2 = (Bird)a3;
}
多态的作用:
假设有三个类Master、Dog、Cat类,Dog和Cat都有eat方法,Master有feed方法(主人喂养宠物),这时主人喂养宠物需要不断地重载方法,没多一种宠物就需要多一个重载的方法,这种做法使得代码之间的耦合度过大,不利于开发
//Master类
public class Master {
public void feed(Cat cat) {
cat.eat();
}
public void feed(Dog dog) {
dog.eat();
}
}
//Dog类
public class Dog {
public void eat() {
System.out.println("狗吃肉");
}
}
//Cat类
public class Cat {
public void eat() {
System.out.println("猫吃鱼");
}
}
于是换一种方法,利用多态+方法重载的方式,新增一个Pet类,使得Cat和Dog继承同一个Pet类,这时在Master类中调用feed方法只需要传递Pet类的对象,无论有多少种宠物,Master类都不需要关心,使得耦合度大大降低
//Master类
public class Master {
//利用方法传递时的向上转型,可以做到将Pet类的子类传递给feed方法
//在编译期调用的时Pet类中的eat方法,所以可以编译通过,实际上执行期调用的是Pet子类中的eat方法
public void feed(Pet p) {
p.eat();
}
}
//Pet类
public class Pet {
public void eat() {
}
}
//Dog类
public class Dog extends Pet {
//重写父类Pet类中的eat方法
public void eat() {
System.out.println("狗吃肉");
}
}
//Cat类
public class Cat extends Pet {
//重写父类Pet类中的eat方法
public void eat() {
System.out.println("猫吃鱼");
}
}
//方法的调用
public class Test {
public static void main(String[] args) {
Master m = new Master();
//在需要传递Cat对象时使用new Cat()的方式
//这样无论传递的对象是什么,都不需要更改Master类中的方法
m.feed(new Cat());
}
}
总结:
1、面向对象编程的核心:定义好类,然后将类实例化为对象,给一个环境驱使以下,让各个对象之间协作起来形成一个系统
2、多态的作用和总结
- 能降低程序的耦合度,提高程序的扩展力
- 能使用多态尽量使用多态
- 父类型引用指向子类型对象
final关键字:
1、final是一个关键字,表示最终的,不可变的
2、final修饰的类无法被继承
3、final修饰的方法无法被覆盖
4、final修饰的变量一旦赋值之后,不可再次赋值
5、final修饰的实例变量必须在声明的同时赋值,或者声明后在构造方法中赋值
6、final修饰的引用一旦指向某个对象之后,不能再指向其他对象,那么被指向的对象无法被垃圾回收器回收,虽然不能再指向其他对象,但是所指向的对象内部的内存是可以被修改的
7、final修饰的实例变量,一般和static联合使用,被称为常量。
//常量语法规范:
//名字全部大写,每个单词之间使用下划线连接
public static final String GUO_JI = "中国";
访问控制权限修饰符:
1、访问控制权限修饰符来控制元素的访问范围
2、访问控制权限修饰符包括:
- public:表示公开的,在任何位置都可以访问
- protected:表示受保护的,在同包或子类中可以访问
- 缺省(default):默认的,在同包下可以访问
- private:表示私有的,只能在本类中访问
3、访问控制权限修饰符可以修饰类、变量、方法…
4、当某个数据只希望子类使用,使用protected进行修饰
5、修饰的范围:
private < 缺省(default) < protected < public
6、类只能采用public和缺省(default)的修饰符进行修饰(内部类除外)
super关键字:
super能出现在实例方法和构造方法中
super关键字的语法:super. 或 super()
super不能使用在静态方法中
super. 大部分情况下是可以省略的
super() 只能出现在构造方法第一行,通过当前的构造方法去调用父类中的构造方法,目的:创建子类对象的时候,先初始化父类型特征
**super():**表示通过子类的构造方法调用父类的构造方法
结论:当一个构造方法第一行既没有this()又没有super()的话,默认会有一个super();表示通过当前子类的构造方法调用父类的无参数构造方法,所以必须保证父类的无参构造方法是存在的
注意:this() 和super()不能共存,它们都是只能出现在构造方法第一行
无论怎么样,父类的构造方法是一定会执行的
Java中允许在子类中出现和父类中的同名属性
什么时候super. 不能省略?
子类和父类中有同名属性或方法,在子类中访问父类的属性或方法时
super不是引用,也不保存内存地址,不指向任何对象,只代表当前对象内部的那一块父类型的特征
抽象类和接口
抽象类:
1、类和类之间具有共同特征,将这些共同特征提取出来,形成的就是抽象类,类本身是不存在的,所以抽象类无法创建对象(无法被实例化)
2、抽象类属于引用数据类型
3、抽象类的定义:
[修饰符列表] abstract class 类名 {
类体;
}
4、抽象类是无法实例化的,无法创建对象,所以抽象类是用来被子类继承的
5、final和abstract不能联合使用
6、抽象类的子类可以是抽象类
7、抽象类虽然无法实例化,但是抽象类有构造方法,这个构造方法是供子类使用的
8、抽象方法:抽象方法表示没有实现的方法,没有方法体的方法,例如:
public abstract void doSome();
//抽象方法特点:1、没有方法体,以分号结尾 2、修饰符列表中有abstract关键字
9、抽象类中不一定有抽象方法,抽象方法必须出现在抽象类中
10、抽象类的子类如果不是抽象类,则一定要重写父类中的所有抽象方法
接口:
1、接口也是一种引用数据类型,编译之后也是一个class字节码文件
2、接口是完全抽象的,也可以说接口是特殊的抽象类
3、接口定义语法:
[修饰符列表] interface 接口名 {
}
4、接口支持多继承,一个接口可以继承多个接口
interface A {
}
interface B {
}
interface C extends A,B {
}
5、接口中只包含两部分内容,一部分是:常量,一部分是:抽象方法
6、接口中所有元素都是public修饰的
7、接口中的抽象方法前面的修饰符列表可以省略不写(public abstract)
8、接口中的方法都是抽象方法,所以接口中的方法都不能有方法体
9、接口中的常量修饰符列表 public static final 可以省略
10、当一个非抽象类实现接口的话,必须将接口中所有的抽象方法全部重写。
11、一个类可以同时实现多个接口
12、继承和实现都存在的话,extends关键字在前,implement关键字在后
13、一个接口只能被另一个接口继承,不能被另一个接口实现
String类:
比较基本数据类型可以使用==,==判断的是内存地址,不是内容
输出一个引用时会自动调用它的toString方法
比较基本数据类型是否相等使用“==”进行判断,比较引用数据类型是否相等使用“equals”方法判断
对于java语言中的“==”操作符号,jvm会根据其两边相互比较的操作数的类型,在编译时生成不同的指令,所以比较基本数据类型时比较的是内容,而比较引用数据类型时比较的是内存地址。
1、String表示字符串类型,属于引用数据类型
2、在Java中使用双引号括起来的都是String对象,例如:“abc”,“123”
3、Java中规定,双引号括起来的字符串是不可变的,也就是说“abc”从出生到死亡,都不会发生改变
4、在JDK当中双引号括起来的字符串,例如:“abc”等都是存储在 方法区内存 中的“字符串常量池”中的,从JDK1.7开始,“字符串常量池”改为放在 堆内存 中,因为字符串在实际的开发中使用太频繁,为了执行效率,所以把字符串放到了常量池中
5、凡是使用双引号括起来的,一定会在常量池中占据一份空间
public static void main(String[] args) {
/*
在这里s1所指向的“abc”放入常量池中,当代码执行到s2时,会在常量池中新建一个“xy”,然后与在执行第一行时放入常量池的“abc”进行拼接,得到一个新的常量“abcxy”,放入常量池中,此时,常量池中有三个字符串常量:“abc”,“xy”,“abcxy”
*/
String s1 = "abc";
String s2 = "abc" + "xy";
/*
在这里通过使用new运算符创建字符串对象,会在堆内存中开辟一块String对象空间,这个空间中保存的是指向常量池中的“abc”的地址,并将堆内存中的这块空间的地址赋给s3
*/
String s3 = new String("abc");
}
//String类的常用方法
public static void main(String[] args) {
//contains(): 判断前一个字符串是否包含后一个字符串,返回true或false
System.out.println("HelloWorld.java".contains(".java"));
//endWith(): 判断当前字符串是否以某个字符串结尾,返回true或false
System.out.println("HelloWorld.java".endWith(".java"));
//判断两个字符串是否相等,忽略大小写,返回true或false
System.out.println("Abc".equalsIgnoreCase("abC"));//true
//getBytes(): 将字符串对象转换成字节数组
byte[] bytes = "abcdef".getBytes();
//indexOf(): 判断某个子字符串在当前字符串中第一次出现处的索引,返回子字符串的第一个字母在当前字符串的下标
System.out.println("oraclemysqljavac++python".indexOf("java"));
//substring(): 截取字符串,传一个参数i表示从下标i开始将字符串剩下的部分截取出来,传两个参数(i,j)表示截取从下标i开始到j-1的字符串
"http://www.baidu.com".substring(7);
//toCharArray(): 将字符串转换成字符数组
"abc".toCharArray();
//toLowerCase(): 将字符串全部转换成小写字母
"ABC".toLowerCase();
//trim(): 去除字符串前面和后面的空白(空格,制表符等),字符串中间的无法去除
" Hello world! ".trim();
//valueOf(): String中的唯一一个静态方法,将不是字符串的数据转换成为字符串,参数是一个对象时,会自动调用该对象的toString方法
String.valueOf(100);
}
StringBuffer:
/*
调用StringBuffer类会创建一个初始化容量为16的char[] 数组(字符串缓冲区),jdk1.9以后是byte[]数组
*/
StringBuffer stringBuffer = new StringBuffer();
/*
拼接字符串,以后拼接字符串尽量调用append()方法,如果char/byte数组满了,会自动扩容
*/
stringBuffer.append("a");
如何优化StringBuffer的性能?
- 在创建StringBuffer的时候尽可能给定一个初始化容量
- 最好减少底层数组的扩容次数,预估一下,给一个合适的初始化容量
StringBuilder:
StringBuilder和StringBuffer用法相同,两者之间的区别:
- StringBuffer的方法都有synchronized关键字修饰,表示StringBuffer在多线程环境下运行是安全的
- StringBuilder的方法都没有synchronized关键字修饰,表示StringBuilder在多线程环境下运行是不安全的
总结:
StringBuffer是线程安全的
StringBuilder是线程不安全的
数组(Array):
1、Java语言中的数组是一种引用数据类型,不属于基本数据类型,父类是object
2、数组实际上是一个容器,可以同时容纳多个元素,是一个数据的集合
3、数组中可以存储基本数据类型,也可以存储引用数据类型的数据
4、数组因为是引用数据类型,所以数组对象是存储在堆内存中的
5、数组中如果存储的是java对象的话,实际上存储的是对象的内存地址,数组中不能直接存储Java对象
6、数组一旦创建,在Java中规定,长度不可变
7、分类:一维数组、二维数组、三维数组、多维数组
8、所有的数组对象都有length属性,用来获取数组中元素的个数
9、Java中的数组要求数组中元素的类型同一,比如:int类型数组只能存储int类型
10、数组在内存方面存储的时候,数组中的元素内存地址是连续的
11、所有的数组都是以数组中第一个元素的地址作为整个数组对象的地址
12、数组的扩容:当一个数组需要扩容时,实际上是重新建立一个更大的数组,然后将原数组中的数据一个一个拷贝到新数组中。
一维数组的定义:
int[] array1;
double[] array2;
一维数组的初始化:
//静态初始化
int[] array1 = {100,200,300};
double[] array2 = {1.1,2.2,3.3}
//动态初始化
int[] array = new int[5];//5表示数组元素个数
二维数组:
二维数组的length属性就是该二维数组的行数
8种包装类:
基本数据类型 | 包装类 |
---|---|
byte | java.lang.Btye |
short | java.lang.Short |
int | java.lang.Integer |
long | java.lang.Long |
float | java.lang.Float |
double | java.lang.Double |
boolean | java.lang.Boolean |
char | java.lang.Character |
八种包装类属于引用数据类型,父类是Object
为什么要使用包装类?
-
因为八种基本数据类型不够用
比如:调用某个方法时需要传一个数字作为参数,但是数字属于基本数据类型,而这个方法参数类型是Object,可见这个方法无法接收基本数据类型的数字,这时就可以传一个数字对应的包装类进去
-
因为java是面向对象的语言,任何操作都是围绕着“对象”来进行的
装箱和拆箱:
基本数据类型转换成引用数据类型称为装箱,反之称为拆箱
//Java中-128-127以内的数比较常用都在“整数型常量池”中所以127以内的数内存地址相同
public static void main(String[] args) {
Integer i = 127;
Integer i1 = 127;
System.out.println(i == i1);//true
i = 128;
i1 = 128;
System.out.println(i == i1);//false
//手动装箱、拆箱
Integer i = new Integer("1");
int a = i.intValue();
//自动装箱、拆箱
Integer i = 1;
int a = i;
}
常用类:
日期类:
Date类和SimpleDateFormat类
//用来获取当前时间等操作
//利用Date类获取当前时间
Date now = new Date();
//设置日期格式
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//获取格式化后的日期并打印
String format = sdf.format(now);
System.out.println(format);//2021-08-20 15:00:22
//将日期字符串转换成Date类型
String s = "2021-08-20 15:00:22";
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date parse = sdf2.parse(s);
System.out.println(parse);//Fri Aug 20 15:00:22 CST 2021
//将毫秒数转换为日期(从1970年1月1日开始计算)
long time = 93471234831278443;
Date date = new Date(time);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss SSS");
sdf.format(date);
//获取从1970年1月1日 00:00:00 000到当前时间的毫秒数
System.out.println(System.currentTimeMillis());//1629443281245
数字类:
//java.text.DecimalFormat 专门负责数字的格式化
/**
数字格式:
# 代表任意数字
, 代表千分位
. 代表小数点
0 代表不够时补0
###,###.## 表示:加入千分位,保留两位小数
*/
DecimalFormat df = new DecimalFormat("###,###.##");//从右往左没三位数加一个, 保留两位小数
String s = df.format(1234567.8777);//1,234,567.88
System.out.println(s);
DecimalFormat df2 = new DecimalFormat("###,###.0000");//保留四位小数,不够补0
String s2 = df2.format(1234.567);//1,234.5670
System.out.println(s2);
随机数类:
//import java.util.Random; 产生随机数
Random r = new Random();
int i = r.nextInt();//随机产生一个int类型取值范围内的数字
//Math.random
//返回一个从某一个整数到另一个整数之间的数
(int)(Math.random()*(上限-下限+1)+下限);
集合:
集合机构关系图(常用集合类,不包含全部):
什么是集合?
数组其实就是一个集合,集合实际上就是一个容器,可以用来容纳其他类型的数据。
为什么说集合在开发中使用较多?
集合是一个容器,是一个载体,可以一次容纳多个对象。在实际开发中,假设连接数据库,数据库当中有10条记录,那么假设把这10条记录查询出来,在java程序中会将10条数据封装成10个java对象,然后将10个java对象放到某一个集合当中,传给前端。
集合中存储的是什么?
集合中存储的都是java对象的内存地址(引用)。
list.add(100);//自动装箱,实际上存储的是Integer
集合在java中本身也是一个容器,一个对象,在任何时候存储的都是java对象的内存地址。
List集合存储元素的特点:
有序可重复
有序:存进去的顺序和取出来的顺序相同,每一个元素都有下标
可重复:存进去一个100,可以再存进去一个100
Set(Map)集合存储元素的特点:
无序不可重复
存进去的顺序和取出来的顺序不一定相同,Set集合中元素没有下标
Map集合的遍历方式:
//map集合的三种遍历方式
Map<Integer,String> map = new HashMap<>();
map.put(1,"zhangsan");
map.put(2,"lisi");
map.put(3,"wangwu");
map.put(4,"zhaoliu");
//1.1 通过获取所有的key进行遍历
Set<Integer> set = map.keySet();
//1.1.1 使用foreach方式遍历
for(Integer i : set) {
System.out.println(map.get(i));
}
//1.1.2 使用Iterator方式遍历
Iterator<Integer> it1 = set.iterator();
while (it1.hasNext()) {
System.out.println(map.get(it1.next()));
}
//1.2 通过获取所有的value进行遍历
Collection<String> values = map.values();
//1.2.1 使用foreach方式遍历
for (String s : values) {
System.out.println(s);
}
//1.2.2 使用Iterator方式遍历
Iterator<String> it2 = values.iterator();
while(it2.hasNext()) {
System.out.println(it2.next());
}
//1.3 通过entrySet进行遍历,先将map集合转化成一个set集合
Set<Map.Entry<Integer, String>> entries = map.entrySet();
//1.3.1 使用foreach方式遍历
for (Map.Entry<Integer, String> m: entries) {
System.out.println(m.getKey() + "----->" + m.getValue());
}
//1.3.2 使用Iterator方式遍历
Iterator<Map.Entry<Integer, String>> it3 = entries.iterator();
while (it3.hasNext()) {
Map.Entry<Integer, String> m = it3.next();
System.out.println(m.getKey() + "----->" + m.getValue());
}
SortedSet(SortedMap)集合存储元素特点:
无序不可重复,但SortedSet集合中的元素是可排序的
可排序:可以按照大小顺序排序
总结(所有的实现类):
-
ArrayList:底层是数组,是线程不安全的,效率较高,初始大小为10,扩容之后是原容量的1.5倍
-
LinkedList:底层是双向链表,插入节点时采用尾插法
-
Vector:底层是数组,是线程安全的,但是效率较低,初始大小为10,扩容之后是原容量的2倍
-
HashSet:底层是HashMap,放到HashSet集合中的元素等同于放到HashMap集合key部分
-
TreeSet:底层是TreeMap,放到TreeSet集合中的元素等同于放到TreeMap集合key部分
-
HashMap:底层是哈希表,是线程不安全的,效率较高
-
Hashtable:底层是哈希表,是线程安全的,效率较低
-
Properties:是线程安全的,并且key和value只能存储字符串String
-
TreeMap:底层是平衡二叉树,TreeMap集合的key可以自动按照大小顺序排序
IO流:
IO流,什么是IO?
I:input
o:output
通过IO可以完成硬盘文件的读和写
IO流的分类?
-
按照流的方向进行分类:
以内存为参照:
输入流(InputStream):往内存中去,叫输入(Input)或读(Read)
输出流(OutputStream):从内存中出来,叫输出(Output)或写(Write)
-
按照读取数据的方式进行分类:
-
字节流:按字节的方式读取数据,一次读取一个字节(byte),等同于一次读取8个二进制
这种流是万能的,可以读取所有类型的文件,包括:文本文件、声音、视频等
-
字符流:按照字符的方式读取数据,一次读取一个字符
这种流是为了方便读取普通文本文件而存在的,但不能读取:图片、声音、视频等文件,只能读取纯文本文件,word也无法读取
-
java中的IO流四大家族:
java.io.InputStream; //字节输入流
java.io.OutputStream; //字节输出流
java.io.Reader; //字符输入流
java.io.Writer; //字符输出流
注意:在java中类名以"Stream"结尾的都是字节流,以"er"结尾的都是字符流
所有的流都实现了 java.io.Closeable 接口,都是可以关闭的,都有close()方法,流在使用完后一定要关闭,否则会占用很多资源,要养成使用完后立即关闭的习惯。
所有的输出流都实现了 java.io.Flushable 接口,都是可以刷新的,养成输出完成后调用 flush() 方法的习惯,表示将未输出完的数据强行输出完(清空管道),否则可能造成数据丢失。
常用且需要掌握的流:
//文件专属
java.io.Fileinputstream
FileInputStream fis = null;
String path = "src/IOStreamStudy/a.txt";
try {
fis = new FileInputStream(path);
int temp;
/**
* available()方法会返回这个文件中剩余没有读的字节数,这样就可以指定固定大小的数据来存放数据,
* 从而不使用循坏也能一次读取完全部数据,但不适合太大的文件
* */
byte bytes[] = new byte[fis.available()];
temp = fis.read(bytes);
System.out.print(new String(bytes,0,temp));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
java.io.Fileoutputstream
java.io.FileReader
java.io.Filewriter
//转换流,将字节流转换成字符流
java.io. InputstreamReader
java.io. outputstreamWriter
//缓冲流
java.io.BufferedReader
java.io.Bufferedwriter
java.io.BufferedInputstream
java.io.Bufferedoutputstream
//数据流
java.io. DataInputstream
java.io.Dataoutputstream
//标准输出流
java.io . Printwriter
java.io . Printstream
//对象专属流
java.io.objectInputstream
java.io. Objectoutputstream
java中默认的起始路径是以当前这个project的为起点的。
复制一个文件
//因为这是使用的字节流,所以是可以读取所有类型的文件的
public static void main(String[] args) {
FileInputStream fis = null;
FileOutputStream fos = null;
String oldPath = "src/IOStreamStudy/a.txt";
String newPath = "src/IOStreamStudy/b.txt";
try {
fis = new FileInputStream(oldPath);
fos = new FileOutputStream(newPath);
byte bytes[] = new byte[fis.available()];
int read = fis.read(bytes);
fos.write(bytes,0,read);
fos.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
/**
* 这里需要分进行异常捕捉,因为如果前一个流报了异常不会影响到后一个流的关闭
* */
if(fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
File类
File类和 流 没有关系,所以File类不能完成文件的读和写
常用方法:
File file = new File("F:/a");
System.out.println(file.exists());//判断文件是否存在
if(!file.exists()) {
try {
file.createNewFile();//以文件的形式新建
} catch (IOException e) {
e.printStackTrace();
}
}
if(!file.exists()) {
file.mkdir();//以文件夹(目录)的形式新建
}
File f2 = new File("F:/a/b/c");
if(!f2.exists()) {
f2.mkdirs();//以多重目录的形式新建
}
String parent = f2.getParent();//获取文件的上一级目录路径
System.out.println(parent);//F:\a\b
String absolutePath = f2.getAbsolutePath();//获取文件的绝对路径
System.out.println(absolutePath);//F:\a\b\c
File f3 = new File("F:/a");
File[] files = f3.listFiles();//获取该目录下所有的子文件
序列化和反序列化
什么是序列化和反序列化?
序列化:将java对象从内存中拷贝到硬盘中的这一过程,称之为序列化
反序列化:将java对象从硬盘中写入内存的过程,称之为反序列化
注意:要序列化的类必须实现Serializeable接口,这个接口中没有任何方法,只是一个标志接口,
它只起到标志的作用,是给jvm参考的,jvm看到这个接口后,会自动为这个类生成一个序列化版本号
public static void main(String[] args) throws Exception {
Student s = new Student("张三");
//序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("src/SerializeStudy/student"));
oos.writeObject(s);
//反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("src/SerializeStudy/student"));
Object o = ois.readObject();
System.out.println(o);
oos.flush();
oos.close();
ois.close();
}
多个对象的序列化:
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("src/SerializeStudy/students"));
ArrayList<Student> studentList = new ArrayList<>();
studentList.add(0,new Student("zhangsan"));
studentList.add(1,new Student("lisi"));
studentList.add(2,new Student("wangwu"));
oos.writeObject(studentList);
oos.flush();
oos.close();
如果一个类中的某个属性被transient关键字修饰了,则这个属性不会参与序列化
private transient String name;
多线程:
什么是线程?什么是进程?
进程是一个应用程序(一个应用程序就是一个软件)
线程是一个进程的执行场景/执行单元
一个进程可以启动多个线程
对于java来说,当在dos命令窗口中输入:java helloworld 回车之后,会先启动jvm,而jvm就是一个进程。jvm再启动一个主线程调用main方法,同时再启动一个垃圾回收线程负责看护,回收垃圾,现在的java程序中至少两个进程,一个是垃圾回收线程,一个是执行main方法的主线程。
在java中:
线程A和线程B,堆内存和方法区内存共享,但是栈内存独立,一个线程一个栈。
假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,互不干扰,各自执行,这就是多线程并发。
实现多线程的三种方法:
一、继承Thread类并重写run方法,由其他类来调用该类的start()方法
public class ThreadTest01 {
public static void main(String[] args) {
MyThread mt = new MyThread();
/**
* 这个方法启动后会开辟一个新的分支栈区,在分支栈区开辟结束后的瞬间,
* 这个方法立马就结束了
* 直接调用 mt.run() 和 mt.start()方法的区别:
* 调用mt.run()是主线程来调用这个方法,会在主栈区中为这个方法开辟内存,
* 不会有新的栈区产生
* 调用mt.start()会开辟新的栈内存空间,会产生一个新的栈空间,在新开辟
* 的分支栈空间中,run()方法和主线程中的main()方法同级
* */
mt.start();
for (int i = 0; i < 1000; i++) {
System.out.println("主线程--->"+i);
}
}
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程--->"+i);
}
}
}
二、实现Runnable接口,重写run()方法,通过Thread类的构造方法来启动线程
public class RunnableTest01 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
for (int i = 0; i < 1000; i++) {
System.out.println("主线程--->"+i);
}
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程--->"+i);
}
}
}
两种方式的区别:
1、实现Runnable接口相对于继承Thread类来说,有如下的显著优势:
- 适合多个相同代码的线程去处理同一个资源的情况
- 可以避免由于java的单继承特性带来的局限
- 增强了程序的健壮性,代码能够被多个线程共享,代码与数据是独立的
2、继承Thread: 线程代码存放Thread子类run方法中。
实现Runnable:线程代码存在接口的子类的run方法。
三、实现Callable接口,利用FutureTask类(jdk8新特性)
/**
* 实现线程的第三种方式:
* 实现Callable接口,
* 优点:可以获取到线程的执行结果
* 缺点:效率较低,想要获取线程执行结果,当前线程会收到阻塞,必须等待这条线程执行结束
* */
public class FutureTaskTest {
public static void main(String[] args) {
FutureTask futureTask = new FutureTask(new MyFuture());
futureTask.run();
try {
Object o = futureTask.get();
System.out.println(o);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyFuture implements Callable {
@Override
public Object call() throws Exception { //等同于run()方法
System.out.println("hello world");
Object o = new Object();
return o;
}
}
线程的生命周期
-
新建状态(new)
使用new创建一个线程对象,仅仅在堆中分配内存空间,在调用start方法之前. 新建状态下,线程压根就没有启动,仅仅只是存在一个线程对象而已.Thread t = new Thread();//此时t就属于新建状态当新建状态下的线程对象调用了start方法,此时从新建状态进入可运行状态.线程对象的start方法只能调用一次,否则报错:IllegalThreadStateException.
-
可运行状态(runnable)
分成两种状态,ready和running。分别表示就绪状态和运行状态。
就绪状态:线程对象调用start方法之后,等待JVM的调度(此时该线程并没有运行)。
运行状态:线程对象获得JVM调度,如果存在多个CPU,那么允许多个线程并行运行。 -
阻塞状态(blocked)
正在运行的线程因为某些原因放弃CPU,暂时停止运行,就会进入阻塞状态。此时JVM不会给线程分配CPU,直到线程重新进入就绪状态,才有机会转到运行状态。阻塞状态只能先进入就绪状态,不能直接进入运行状态。
阻塞状态的两种情况:
1):当A线程处于运行过程时,试图获取同步锁时,却被B线程获取。此时JVM把当前A线程存到对象的锁池中,A线程进入阻塞状态。
2):当线程处于运行过程时,发出了IO请求时,此时进入阻塞状态.。 -
等待状态(waiting)(等待状态只能被其他线程唤醒):
此时使用的是无参数的wait方法,当线程处于运行过程时,调用了wait()方法,此时JVM把当前线程存在对象等待池中。
-
计时等待状态(timed waiting)(使用了带参数的wait方法或者sleep方法):
1):当线程处于运行过程时,调用了**wait(long time)方法,此时JVM把当前线程存在对象等待池中。
2):当前线程执行了sleep(long time)**方法。 -
终止状态(terminated):
通常称为死亡状态,表示线程终止。
1):正常执行完run方法而退出(正常死亡).
2):遇到异常而退出,出现异常之后,程序就会中断(意外死亡)。
多线程常用方法
当前线程
currentThread();方法可以调用当前线程,类似于this关键字
线程名字
setNmae();方法可以为当前线程对象设置一个线程名
getName();方法获取当前线程名字,一般和currentThread();方法配合使用
休眠
调用 Thread.sleep(long time);方法可以使当前调用这个方法的线程休眠,和调用对象无关,time单位为毫秒
public class SleepTest01 {
public static void main(String[] args) {
MyThread2 mt2 = new MyThread2();
mt2.start();
try {
//在这里虽然是mt2线程对象调用的sleep()方法,但是由于调用的线程仍然是主线程,所以休眠的还是主线程
mt2.sleep(1000 * 3);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
class MyThread2 extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
唤醒线程
public class SleepTest01 {
public static void main(String[] args) {
MyThread2 mt2 = new MyThread2();
mt2.start();
try {
Thread.sleep(1000 * 3);
} catch (InterruptedException e) {
e.printStackTrace();
}
mt2.interrupt();
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
class MyThread2 extends Thread {
public void run() {
System.out.println("分支begin");
try {
interrupt();
sleep(1000*60);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("分支end");
}
}
终止线程
调用stop()方法可以终止一条线程的运行,但这种方式已经不可取了,因为有可能造成数据的丢失或损坏。
使用下面这种方式来终止一条线程:
public class ThreadTest02 {
public static void main(String[] args) {
MyThread3 mt3 = new MyThread3();
Thread t = new Thread(mt3);
t.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
mt3.run = false;
}
}
class MyThread3 implements Runnable {
boolean run = true;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(run) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
return;
}
}
}
}
线程安全(重要)
什么时候会出现线程安全问题?
- 多线程并发
- 有共享数据
- 共享数据有修改的行为
怎么解决线程安全问题?
使用“线程同步机制”,所谓线程同步,就是让线程进行排队,在一个线程访问共享数据时阻止其他线程对这个数据的访问。虽然线程同步机制会降低一部分效率,但是只有保证线程安全了,才能谈效率,所以这是是非必要的。
编程模型:
异步编程模型:
线程t1和t2,各自执行,两者之间没有等待关系,互不相干。
同步编程模型:
在线程t1执行的时候,线程t2需要等待线程t1的执行结束,才能继续执行。
synchronized关键字:
public class ThreadTest03 {
public static void main(String[] args) {
Account act = new Account(10000);
ActThread at1 = new ActThread(act);
ActThread at2 = new ActThread(act);
Thread t1 = new Thread(at1);
Thread t2 = new Thread(at2);
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
class Account {
private double balance;
public Account(double balance) {
this.balance = balance;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public void withdrwa(double money) {
/**
* 这里的括号里面的参数不一定是this,只要是线程共享的对象就行,
* 比如有线程t1~t5,想让t1~t3排队,t4,t5不排队,则只需要填写
* t1~t3共享t4,t5不共享的对象就行了
* */
synchronized (this) {
double befor = getBalance();
double after = befor - money;
try {
Thread.sleep(500);//模拟网络延迟500ms,必定出现线程安全问题
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
System.out.println("线程" + Thread.currentThread().getName() + "取款成功,余额:"+this.getBalance());
}
}
}
class ActThread implements Runnable {
private Account act;
public ActThread(Account act) {
this.act = act;
}
@Override
public void run() {
act.withdrwa(5000);
}
}
java中会出现线程安全问题的变量:
Java中的三大变量:
实例变量(全局变量):在堆内存中
静态变量:在方法区内存中
局部变量:在栈内存中
以上三个变量中,局部变量永远都不会存在线程安全问题,因为局部变量不共享,一个线程会开辟一个栈空间,局部变量处于不同的栈空间中,所以永远都不会有线程安全的问题。
堆内存和方法区内存只有一个,又都是多线程共享的,所以有可能存在线程安全问题。
局部变量使用String类型时,因为局部变量没有线程安全问题,而StringBuilder比StringBuffer效率更高,所以建议使用StringBuilder
synchronized的三种用法:
一、同步代码块:
synchronized () {
代码块;
}
二、在实例方法上使用:
public synchronized void method() {
//当共享对象是this,并且同步代码块是整个方法体时,建议使用这种方式,可以使代码更简洁
}
三、在静态方法上使用
public synchronized static void method() {
//这种方式表示找类锁,也就是这个静态方法所在的类,类锁永远只有1把,不管创建几个对象,类锁也只有一把
}
线程死锁
public class DeadLock {
public static void main(String[] args) {
Object obj1 = new Object();
Object obj2 = new Object();
MyThreadTest1 mtt1 = new MyThreadTest1(obj1,obj2);
MyThreadTest2 mtt2 = new MyThreadTest2(obj1,obj2);
mtt1.start();
mtt2.start();
}
}
class MyThreadTest1 extends Thread {
Object obj1;
Object obj2;
public MyThreadTest1(Object obj1, Object obj2) {
this.obj1 = obj1;
this.obj2 = obj2;
}
@Override
public void run() {
synchronized (obj1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2) {
}
}
}
}
class MyThreadTest2 extends Thread {
Object obj1;
Object obj2;
public MyThreadTest2(Object obj1, Object obj2) {
this.obj1 = obj1;
this.obj2 = obj2;
}
@Override
public void run() {
synchronized (obj2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj1) {
}
}
}
}
在java中要尽量避免这种情况的出现,否则可能会造成死锁,尽量不要使用synchronized嵌套
守护线程
守护线程可以理解为守护其他线程的线程,当主线程结束时,守护线程也会自动结束,类似QQ聊天窗口和QQ的关系,当关闭QQ时,聊天窗口会一起被关闭。
设置守护线程的方法
Thread t = new Thread;
t.setDaemon(true);//设置好这个后,这个线程就变成了守护线程
定时器
间隔特定的时间,执行特定的程序
比如:每天备份数据,银行每周进行账户的转账
java类库中的定时器:java.util.Timer
设置一个定时任务:
/**
* 首先定义一个类,继承抽象类TimerTask,并重写run方法,
* 然后将某个时间作为第一次任务开始的时间,作为参数传给
* Timer类中的schedule方法。
* @method Timer.schedule(TimerTask task, Date date, long period);
* 设置要执行的任务,第一次执行的时间,此后每隔多久执行一次
* */
public class TimerTest01 {
public static void main(String[] args) {
Timer t = new Timer();
Date parse;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
parse = sdf.parse("2021-09-30 23:47:00");
t.schedule(new TimerTaskTest(),parse,3000);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
class TimerTaskTest extends TimerTask {
@Override
public void run() {
System.out.println("执行了一次定时任务");
}
}
生产者和消费者
/**
* 如果使用的包装类作为锁对象,并且包装类对象的值是会变化的,则会导致锁对象的地址一直改变
* 从而导致锁失效,出现java.lang.IllegalMonitorStateException异常
* */
public class ThreadTest04 {
public static void main(String[] args) {
List list = new ArrayList();
Thread t1 = new Thread(new Producer(list));
Thread t2 = new Thread(new Consumer(list));
t1.setName("生产者线程");
t2.setName("消费者线程");
t1.start();
t2.start();
}
}
class Producer implements Runnable {
private List list;
public Producer(List list) {
this.list = list;
}
@Override
public void run() {
while(true) {
synchronized (list) {
if(list.size() >= 3) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 0; i < 3; i++) {
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName() + "--->" + list.get(i));
}
list.notify();
}
}
}
}
class Consumer implements Runnable {
private List list;
public Consumer(List list) {
this.list = list;
}
@Override
public void run() {
while(true) {
synchronized (list) {
if(list.size() == 0) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 0; i < 3; i++) {
Object remove = list.remove(0);
System.out.println(Thread.currentThread().getName() + "--->" + remove);
}
list.notify();
}
}
}
}
反射
反射有什么用?
通过java中的反射机制可以操作字节码文件
可以读和写字节码文件(class文件)
反射所在包:java.lang.reflect.*;
反射常用类:
java.lang.Class;//类
java.lang.reflect.Method;//方法
java.lang.reflect.Field;//属性(成员变量)
java.lang.reflect.Constructor;//构造方法
获取class的三种方式及如何创建对象:
/*
通过反射获取class(字节码文件)的三种方式以及创建对象
Class类:java.lang.Class
*/
public class ClassTest01 {
public static void main(String[] args) {
//1.forNmae:Class中的静态方法,通过指定完整的类名来获取class
Class d = null;
try {
d = Class.forName("java.util.Date");//d在这是就代表Date.class
System.out.println(d);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//2.getClass()方法,从Object类中继承过来的方法,即每个类都可以调用这个方法,从而获取该类的class
String str = "abc";
Class s = str.getClass();//s表示String.class
System.out.println(s);
//3.在java中,所有的数据类型都有".class"属性,包括基本数据类型和引用数据类型
Class i = int.class;
System.out.println(i);
Class date = Date.class;
System.out.println(d == date);//true
//利用反射创建对象
try {
/*
newInstance()方法底层会调用这个类的无参构造方法,所以利用这种方法创建对象时,
一定要保证该类的无参构造是存在的
从jdk9之后,这个方法就过时了
*/
Object o = date.newInstance();
System.out.println(o);//Sun Oct 03 09:34:11 CST 2021
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
反射和普通方式创建对象的区别:
/**
* 普通方式创建对象和反射创建对象的对比
* 通过反射创建对象只需要修改配置文件即可达到创建不同的对象,
* 而普通方式只能写死,创建一个特定的类,不够灵活
* 修改配置文件中的className可以达到灵活创建对象的目的,也是各种框架的核心部分
*/
public class ClassTest02 {
public static void main(String[] args) throws Exception {
//不使用反射创建对象:
Student s = new Student(1,"张三");
System.out.println(s);//Student{id=1, name='张三'}
//使用反射创建对象
FileInputStream fis = null;
fis = new FileInputStream("src/test.properties");
Properties p = new Properties();
p.load(fis);
fis.close();
String className = p.getProperty("className");
System.out.println(className);//ReflectStudy.Student
Class student = Class.forName(className);
System.out.println(student);//class ReflectStudy.Student
Object o = student.newInstance();
System.out.println(o);//Student{id=0, name='null'}
}
}
class Student {
private int id;
private String name;
public Student() {
}
public Student(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
className=ReflectStudy.Student
IO+Properties的改写:
//通过资源绑定器:ResourceBundle类
//IO+Properties快速绑定".properties"配置文件,后面不需要加后缀,这种方式只能绑定properties文件,并且文件一定要在类路径下
ResourceBundle rb = ResourceBundle.getBundle("test");
String className = rb.getString("className");
System.out.println(className);
只让静态代码块执行:
public class ClassTest03 {
public static void main(String[] args) throws Exception {
/*
* 如果只想让某个类的静态代码块执行,其它代码不执行,
* 可以直接使用Class.forName()方法,这个方法会导致类加载
* */
Class.forName("ReflectStudy.Myclass");
}
}
class Myclass {
static {
System.out.println("Myclass静态代码块执行了");
}
}
获取类路径下文件的绝对路径:
public class AbsolutePathTest {
public static void main(String[] args) {
/*
* 使用这种方式,一旦换了开发环境就会无法正确的获取到路径
* FileInputStream fis = new FileInputStream("src/test.properties");
*
* 以下写法是通用的:
* 获取某个文件的绝对路径,前提是这个文件必须在类路径下(src文件下)
* Thread.currentThread() 获取当前线程对象
* getContextClassLoader() 线程对象的方法,获取当前线程对象的类加载器
* getResource() 类加载器的方法,当前线程的类加载器默认从类的根路径下加载资源
* */
String path = Thread.currentThread().getContextClassLoader().getResource("test.properties").getPath();
System.out.println(path);
}
}
//以流的形式访问
InputStream reader = Thread.currentThread().getContextClassLoader().getResourceAsStream("test.properties");
Properties p = new Properties();
p.load(reader);
reader.close();
String className = p.getProperty("className");
System.out.println(className);
关于类加载器的扩展:
1、什么是类加载器?
专门负责加载类的命令/工具
ClassLoader
2、jdk中自带了3个类加载器
启动类加载器
扩展类加载器
应用类加载器
3、假设有这样的一段代码:
String s = “abc”;
代码在开始执行之前,会将所需要的类全部加载到jvm中,
通过类加载器加载,看到以上代码类加载器会找String.class
文件,找到就加载。
首先通过“启动类加载器”加载
注意:启动类加载器专门加载:corretto-1.8.0_275\jre\lib\rt.jar
如果通过“启动类加载器”加载不到的时候
会通过“扩展类加载器“加载
注意:扩展类加载器专门加载:corretto-1.8.0_275\jre\lib\ext\*.jar
如果通过“扩展类加载器”加载不到的时候
会通过“应用类加载器“加载
注意:应用类加载器专门加载:classpath中的jar包文件
4、java中为了保证类加载的安全,使用了双亲委派机制
优先从启动类加载器中加载,这个称为”父“,”父“无法加载到,
再从扩展类加载器中加载,这个称为”母“。双亲委派,如果都加载 不到,才会考虑从应用类加载器中加载,直到加载到为止。
Field类
使用java.lang.reflect.Field类,可以获取到一个类的所有属性
public class FieldTest01 {
public static void main(String[] args) throws Exception {
//Field类的常用方法
Class stu = Class.forName("ReflectStudy.Student02");
//获取一个类的完整类名
System.out.println(stu.getName());
//获取一个类的简单类名(去除前面的包名)
System.out.println(stu.getSimpleName());
//返回一个类的所有公共成员属性
Field[] fields = stu.getFields();
//获取一个类的所有成员属性
Field[] declaredFields = stu.getDeclaredFields();
for(Field f: declaredFields) {
//获取数据类型
System.out.println(f.getType());
//获取变量名字
System.out.println(f.getName());
//获取修饰符列表,返回的是修饰符的数字代码
int modifiers = f.getModifiers();
System.out.println(modifiers);
//利用Modifier类中的静态toString()方法,可以将数字代码转换成修饰符
System.out.println(Modifier.toString(modifiers));
}
}
}
class Student02 {
private int id;
protected String name;
boolean sex;
public int age;
public static final double MATH_PI = 3.1415926535;
}
通过反射给对象的属性赋值:
//通过反射机制访问类中的属性
public class FieldTest02 {
public static void main(String[] args) throws Exception {
//不使用反射给类中的属性赋值
Student02 s = new Student02();
s.name = "李四";
System.out.println(s.name);
//使用反射给类中的属性赋值,虽然代码更加复杂了,但是灵活性却大大提高了
Class student = Class.forName("ReflectStudy.Student02");
Object o = student.newInstance();
//通过getDeclaredField()方法,可以指定属性名来获取某个属性
Field name = student.getDeclaredField("name");
//Field的set()方法,给类中的属性赋值
name.set(o,"张三");
System.out.println(o);
//获取某个类中的name属性
Object o1 = name.get(o);
System.out.println(o1);
//获取私有属性
Field id = student.getDeclaredField("id");
//打破id的封装,容易留下安全隐患
id.setAccessible(true);
//没有使用setAccessible()方法时在这里报了一个错误:java.lang.IllegalAccessException,说明私有属性无法被直接获取
id.set(o,123);
System.out.println(id.get(o));
}
}
Method类:
//Method类的常用方法以及反射调用方法(重要)
public class MethodTest01 {
public static void main(String[] args) throws Exception {
//不通过反射调用方法
UserService us = new UserService();
System.out.println(us.login("admin", "123"));
//反射获取Method对象
Class userService = Class.forName("ReflectStudy.UserService");
//创建userService对象
Object o = userService.newInstance();
//获取userService的所有方法
Method[] declaredMethods = userService.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
//获取方法名
System.out.println(declaredMethod.getName());
//获取返回值类型
System.out.println(declaredMethod.getReturnType().getSimpleName());
//获取修饰符列表
System.out.println(Modifier.toString(declaredMethod.getModifiers()));
//获取参数类型列表
Class<?>[] parameterTypes = declaredMethod.getParameterTypes();
for (Class<?> parameterType : parameterTypes) {
System.out.println(parameterType.getSimpleName());
}
}
//怎么通过反射调用方法?重点内容:框架核心部分
/*
* 先获取Method对象,用invoke方法调用
* name: 类对象中的方法名,后两个Stirng.class表示方法的参数列表
* invoke()作用:
* 调用o对象中的declaredMethod方法,以"admin","123456"作为参数传递过去,
* resultType:declaredMethod方法的返回值
* */
Method declaredMethod = userService.getDeclaredMethod("login", String.class, String.class);
Object resultType = declaredMethod.invoke(o, "admin","123456");
System.out.println(resultType);
}
}
class UserService {
public boolean login(String usernmae, String password) {
if("admin".equals(usernmae) && "123".equals(password))
return true;
return false;
}
public void logout() {
System.out.println("退出登录");
}
}
Constructor类:
//反射获取构造方法
public class ConstructorTest01 {
public static void main(String[] args) throws Exception{
//不使用反射
Vip v = new Vip();
Vip v1 = new Vip(1);
//使用反射
Class vip = Class.forName("ReflectStudy.Vip");
//所有构造方法
Constructor[] declaredConstructors = vip.getDeclaredConstructors();
for (Constructor declaredConstructor : declaredConstructors) {
System.out.println(declaredConstructor.getName());
//参数列表等操作参考Method
System.out.println(Modifier.toString(declaredConstructor.getModifiers()));
}
//调用无参构造方法
Object o = vip.newInstance();
//调用无参构造方法2,在jdk9的newInstance()方法过时后,官方的推荐调用无参构造的方式
//先获取到有参构造方法,区分有参构造方法只有参数列表
Constructor dc = vip.getDeclaredConstructor();
Object o2 = dc.newInstance();
//调用有参构造方法
//先获取到有参构造方法,区分有参构造方法只有参数列表,括号中只需要传递参数的class对象就行
Constructor declaredConstructor = vip.getDeclaredConstructor(int.class);
//使用newInstance()方法来调用有参,在这里写上具体的参数,类型和上面的int.class对应,返回一个对应的类的对象
Object o1 = declaredConstructor.newInstance(10);
//反射获取类的父类
Class superclass = vip.getSuperclass();
System.out.println(superclass.getName());
//获取类的父接口
Class[] interfaces = vip.getInterfaces();
for (Class interface1 : interfaces) {
System.out.println(interface1.getName());
}
}
}
class Vip implements ActionListener {
int time;
int level;
String name;
String endTime;
public Vip() {
}
public Vip(int time) {
this.time = time;
}
public Vip(int time, int level) {
this.time = time;
this.level = level;
}
public Vip(int time, int level, String name) {
this.time = time;
this.level = level;
this.name = name;
}
public Vip(int time, int level, String name, String endTime) {
this.time = time;
this.level = level;
this.name = name;
this.endTime = endTime;
}
@Override
public void actionPerformed(ActionEvent e) {
}
}
注解
什么是注解?
注解:也叫做注释,英文单词:Annotation
注解时一种引用数据类型,编译之后也生成xxx.class文件
自定义注解的语法格式
[修饰符列表] @interface 注解名 {
}
public @interface MyAnnotation {
}
注解怎么用,用在哪?
使用注解的语法格式:@注解名
注解可以出现在类上,属性上,方法上,变量上等
注解还可以出现在注解类型上,用来标注“注解类型”的注解,叫做元注解,常见元注解:
/*
这个注解用来表示被标注的注解可以出现在什么地方
@Target(ElementType.METHOD)表示这个注解只能出现在方法上
*/
@Target
/*
这个注解表示被标注的注解最终保存在哪里
@Retention(RetentionPolicy.SOURCE):表示该注解只被保留在源文件中
@Retention(RetentionPolicy.CLASS):表示注解被保存在class文件中
@Retention(RetentionPolicy.RUNTIME):表示注解被保存在class文件中,并且可以被反射机制读取到
*/
@Retention
注解中定义属性:
public @interface MyAnnotation {
//这个看着像是一个方法,实际上在注解中它表示一个属性
String name();
}
注解属性的使用:
public class AnnotationTest01 {
public static void main(String[] args) {
//这个注解在这里报错的原因:如果一个注解中定义了属性,那么必须给属性赋值
// @MyAnnotation
// int i;
//正确写法
@MyAnnotation(name = "张三")
String name;
}
}
注解的默认值:
public @interface MyAnnotation {
//这个看着像是一个方法,实际上在注解中它表示一个属性
String name();
//用这种方法可以给注解的属性赋一个默认值,这样被标注时这个属性就可以不被赋值
int age() default 20;
}
public class AnnotationTest01 {
public static void main(String[] args) {
//在这里没有为age赋值,但仍然没有报错,因为age有默认值
@MyAnnotation(name = "张三")
String name;
}
}
当属性是value时,可以省略value
public @interface MyAnnotation {
String value();
}
public class AnnotationTest01 {
public static void main(String[] args) {
/*
* "value ="可以省略的条件:
* 当注解中的属性有且只有一个,并且属性名为value()时,为value赋值时"value ="可以省略不写
* */
@MyAnnotation("张三")
String name;
}
}
反射获取注解:
//通过反射获取注解和注解中的属性值
public class AnnotationTest02 {
public static void main(String[] args) throws Exception {
//获取class
Class a1 = Class.forName("AnnotationStudy.AnnotationTest01");
//判断是否被MyAnnotation注解标注
if(a1.isAnnotationPresent(MyAnnotation.class)) {
//被MyAnnotation注解标注了,则获取注解的value值
MyAnnotation annotation = (MyAnnotation)a1.getAnnotation(MyAnnotation.class);
System.out.println(annotation.value());
}
//获取方法上的注解与获取类上的注解方式一样,只是需要先获取到method对象
}
}
//加上这个注解,则这个注解可以被反射获取到
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String value();
}