1. 枚举
1.1 学习目标
- 了解枚举的概念
- 掌握枚举的格式
- 掌握枚举的应用场景
- 掌握枚举的使用
1.2 内容讲解
1.2.1 枚举的概述
枚举是 Java 中一种特殊的类,它可以定义固定数量的枚举实例,例如: 性别、交通信号灯、季节等等。
1.2.2 为什么要使用枚举
假设我们要定义一个人类,人类中包含姓名和性别。通常会将性别定义成字符串类型,效果如下:
public class Person { private String name; private String sex; public Person() { } public Person(String name, String sex) { this.name = name; this.sex = sex; } // 省略get/set/toString方法 }
public class Demo01 { public static void main(String[] args) { Person p1 = new Person("张三", "男"); Person p2 = new Person("张三", "abc"); // 因为性别是字符串,所以我们可以传入任意字符串 } }
不使用枚举存在的问题:可以给性别传入任意的字符串,导致性别是非法的数据,不安全。
1.2.3 作用
一个方法接收的参数是固定范围之内的时候,那么即可使用枚举类型
1.2.4 格式
enum 枚举名 { 第一行都是罗列枚举实例,这些枚举实例直接写大写名字即可。 }
入门案例:
枚举的本质是一个类,所以枚举中还可以有成员变量,成员方法等。
(1)定义枚举:MALE表示男,FEMALE表示女
public enum Gender { /** * 男 */ MALE("男"), /** * 女 */ FEMALE("女"); private String tag; Gender(String tag) { this.tag = tag; } public String getTag() { return tag; } }
(2)Perosn中的性别有String类型改为Sex枚举类型
public class Person { private String name; private Gender gender; public Person() { } public Person(String name, Gender gender) { this.name = name; this.gender = gender; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", gender=" + gender + '}'; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Gender getGender() { return gender; } public void setGender(Gender gender) { this.gender = gender; } }
public class TestEnum { public static void main(String[] args) { Person person = new Person("张三",Gender.MALE); System.out.println(person); //我们怎么才能获取到用户的性别的标记 System.out.println(Gender.MALE.getTag()); } }
2. 反射
2.1 学习目标
- 了解类的加载过程
- 理解类初始化过程
- 了解类加载器
- 掌握获取Class对象的四种方式
- 能够运用反射获取类型的详细信息
- 能够运用反射动态创建对象
- 能够运用反射动态获取成员变量并使用
- 能够运用反射动态获取成员方法并使用
- 能够运用反射获取泛型父类的类型参数
2.2 内容讲解
2.2.1 类加载(了解)
类在内存中的生命周期:加载-->使用-->卸载
2.2.2 类的加载过程
当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、连接、初始化三个步骤来对该类进行初始化,如果没有意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载。
类的加载又分为三个阶段:
(1)加载:load
就是指将类型的class字节码数据读入内存
(2)连接:link
①验证:校验合法性等
②准备:准备对应的内存(方法区),创建Class对象,为类变量赋默认值,为静态常量赋初始值。
③解析:把字节码中的符号引用替换为对应的直接地址引用
(3)初始化:initialize(类初始化)即执行<clinit>类初始化方法,会给类的静态变量赋初始值
2.2.3 类的初始化
哪些操作会导致类的初始化?
(1)运行主方法所在的类,要先完成类初始化,再执行main方法
(2)第一次使用某个类型就是在new它的对象,此时这个类没有初始化的话,先完成类初始化再做实例初始化
(3)调用某个类的静态成员(类变量和类方法),此时这个类没有初始化的话,先完成类初始化
(4)子类初始化时,发现它的父类还没有初始化的话,那么先初始化父类
(5)通过反射操作某个类时,如果这个类没有初始化,也会导致该类先初始化
class Father{ static{ System.out.println("main方法所在的类的父类(1)");//初始化子类时,会初始化父类 } } public class TestClinit1 extends Father{ static{ System.out.println("main方法所在的类(2)");//主方法所在的类会初始化 } public static void main(String[] args) throws ClassNotFoundException { new A();//第一次使用A就是创建它的对象,会初始化A类 B.test();//直接使用B类的静态成员会初始化B类 Class clazz = Class.forName("com.atguigu.test02.C");//通过反射操作C类,会初始化C类 } } class A{ static{ System.out.println("A类初始化"); } } class B{ static{ System.out.println("B类初始化"); } public static void test(){ System.out.println("B类的静态方法"); } } class C{ static{ System.out.println("C类初始化"); } }
哪些使用类的操作,但是不会导致类的初始化?
(1)使用某个类的静态的常量(static final)
(2)通过子类调用父类的静态变量,静态方法,只会导致父类初始化,不会导致子类初始化,即只有声明静态成员的类才会初始化
(3)用某个类型声明数组并创建数组对象时,不会导致这个类初始化
public class TestClinit2 { public static void main(String[] args) { System.out.println(D.NUM); System.out.println(F.num); F.test(); G[] arr = new G[5]; } } class D{ public static final int NUM = 10; static{ System.out.println("D类的初始化"); } } class E{ static int num = 10; static{ System.out.println("E父类的初始化"); } public static void test(){ System.out.println("父类的静态方法"); } } class F extends E{ static{ System.out.println("F子类的初始化"); } } class G{ static{ System.out.println("G类的初始化"); } }
2.2.4 类的加载器
很多开发人员都遇到过java.lang.ClassNotFoundException或java.lang.NoClassDefError,想要更好的解决这类问题,或者在一些特殊的应用场景,比如需要支持类的动态加载或需要对编译后的字节码文件进行加密解密操作,那么需要你自定义类加载器,因此了解类加载器及其类加载机制也就成了每一个Java开发人员的必备技能之一。分为以下几种:
(1)引导类加载器(Bootstrap Classloader)又称为根类加载器
它负责加载jre/lib中的核心库
它本身不是Java代码实现的,也不是ClassLoader的子类,获取它的对象时往往返回null
(2)扩展类加载器(Extension ClassLoader)
它负责加载jre/lib/ext扩展库
它是ClassLoader的子类
(3)应用程序类加载器(Application Classloader)
它负责加载项目的classpath路径下的类
它是ClassLoader的子类
(4)自定义类加载器
当你的程序需要加载“特定”目录下的类,可以自定义类加载器;
当你的程序的字节码文件需要加密时,那么往往会提供一个自定义类加载器对其进行解码
后面会见到的自定义类加载器:tomcat中
2.2.5 Java系统类加载器的双亲委托模式
简单描述:
下一级的类加载器,如果接到任务时,会先搜索是否加载过,如果没有,会先把任务往上传,如果都没有加载过,一直到根加载器,如果根加载器在它负责的路径下没有找到,会往回传,如果一路回传到最后一级都没有找到,那么会报ClassNotFoundException或NoClassDefError,如果在某一级找到了,就直接返回Class对象。
应用程序类加载器 把扩展类加载器视为父加载器,
扩展类加载器 把 引导类加载器视为父加载器。
不是继承关系,是组合的方式实现的。
2.2.6 java.lang.Class类
Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。
要想解剖一个类,必须先要获取到该类的Class对象。而剖析一个类或用反射解决具体的问题就是使用相关API(1)java.lang.Class(2)java.lang.reflect.*。所以,Class对象是反射的根源。
哪些类型可以获取Class对象呢?
所有的Java类型,如:
//(1)基本数据类型和void 例如:int.class void.class //(2)类和接口 例如:String.class Comparable.class //(3)枚举 例如:ElementType.class //(4)注解 例如:Override.class //(5)数组 例如:int[].class
2.2.7 获取Class对象的四种方式
(1)类型名.class
要求编译期间已知类型
(2)对象.getClass()
获取对象的运行时类型
(3)Class.forName(类型全名称),通常需要配置文件配置配合使用
可以获取编译期间未知的类型
(4)ClassLoader的类加载器对象.loadClass(类型全名称)
可以用系统类加载对象或自定义加载器对象加载指定路径下的类型
public class TestReflect { public static void main(String[] args) throws Exception{ //第一种方式获取Class:类型名.class Class personClass = Person.class; /*System.out.println(personClass);*/ //第二种方式获取Class:对象.getClass() Person person = new Person(); Class<? extends Person> aClass = person.getClass(); /*System.out.println(aClass); System.out.println(aClass==personClass);*/ //第三种方式获取Class:Class.forName("类的全限定名") Class<?> aClass1 = Class.forName("com.lxt.item2.Person"); //1.使用反射获取包名 Package aPackage = personClass.getPackage(); System.out.println(aPackage); String name = aPackage.getName(); System.out.println(name); //2.获取修饰符 int modifiers = personClass.getModifiers(); //修饰符的描述是定义在jdk中的Modifier类中,使用常量定义的 System.out.println(modifiers); //3.获取类名,getName是获取类的全限定名,getSimpleName是获取类的名字 String name1 = personClass.getName(); System.out.println(name1); System.out.println(personClass.getSimpleName()); //4.获取父类的字节码对象 Class superclass = personClass.getSuperclass(); System.out.println(superclass.getSimpleName()); //5.获取该类实现的所有接口的字节码对象 Class[] interfaces = personClass.getInterfaces(); for(Class i : interfaces){ System.out.println(i); } //6.获取类的所有成员变量(非静态) Field[] declaredFields = personClass.getDeclaredFields(); for(Field f : declaredFields){ System.out.println(f.getName()); } //7.获取类的所有构造函数 Constructor[] declaredConstructors = personClass.getDeclaredConstructors(); for(Constructor c:declaredConstructors){ System.out.println(c); } //8.获取类的所有方法 Method[] declaredMethods = personClass.getDeclaredMethods(); for (Method declaredMethod : declaredMethods) { System.out.println(declaredMethod.getName()); } } }
2.2.8 反射的概念
反射是一种机制/功能,利用该机制/功能可以在程序运行过程中对类进行解剖并操作类中的构造方法,成员方法,成员属性。
2.2.9 反射的应用场景
各种框架的设计(主要场景)
各大框架的内部实现也大量使用到了反射机制,所以要想学好这些框架,则必须要求了解反射机制。
Person类
public class Person implements Hello { private int age = 10; private String name = "张三"; public String address = "深圳"; public Person() { System.out.println("执行了无参构造"); } public Person(String name, String address) { this.name = name; this.address = address; } public Person(int age, String name) { this.age = age; this.name = name; } public Person(int age, String name, String address) { this.age = age; this.name = name; this.address = address; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public Integer getName(Integer n) { return n; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } @Override public String toString() { return "Person{" + "age=" + age + ", name='" + name + '\'' + ", address='" + address + '\'' + '}'; } @Override public void sayHello() { System.out.println("hello world"); } private void study(String course, int day) { System.out.println("努力学习:" + course + ",学习" + day + "天"); } }
public class TestCreate { public static void main(String[] args) throws Exception{ //获取Person类的Class对象,也就是字节码文件的对象 Class personClass = Person.class; //使用第一种方式创建Person类的对象 //强转一定是建立在父子关系的前提下 /*Person per1 = (Person)personClass.newInstance(); System.out.println(o);*/ //使用第二种方式创建Person类的对象 //获取无参的构造函数 /*Constructor declaredConstructor = personClass.getDeclaredConstructor(); Person per2 = (Person)declaredConstructor.newInstance(); System.out.println(per2);*/ //获取有参的构造函数 Constructor declaredConstructor = personClass.getDeclaredConstructor(int.class, String.class); Person per3 = (Person)declaredConstructor.newInstance(12, "李四"); System.out.println(per3); } }
public class TestMethod { public static void main(String[] args)throws Exception{ Class personClass = Person.class; //1.获取某一个方法,例如:getName() //获取无参的getName方法 Method getName = personClass.getDeclaredMethod("getName",Integer.class); Person o = (Person)personClass.newInstance(); /*String str = (String)getName.invoke(o); System.out.println(str);*/ Integer invoke = (Integer)getName.invoke(o,1); System.out.println(invoke); //暴力反射 Method study = personClass.getDeclaredMethod("study",String.class,int.class); study.setAccessible(true); Object invoke1 = study.invoke(o,"语文",12); } }
public class TestProperty { public static void main(String[] args) throws Exception{ Class personClass = Person.class; Object o = personClass.newInstance(); //2.1 获取Person的所有属性(只能获取自己的,包含公有的和私有的) Field[] declaredField = personClass.getDeclaredFields(); for(Field d : declaredField){ //获取每个属性的属性名和属性值 //获取属性名 String name = d.getName(); //获取属性的类型 Class<?> type = d.getType(); //获取属性的修饰符 int modifiers = d.getModifiers(); //暴力反射:通过反射可以访问类的私有成员 d.setAccessible(true); //获取属性的值 Object value = d.get(o);//等于 对象.属性名 System.out.println(name + "," + value + "," + type + "," + modifiers); } //2.2 单独获取某一个属性,比如获取name Field address = personClass.getDeclaredField("address"); address.set(o,"北京"); Object o1 = address.get(o); System.out.println(o1); } }
2.3 Type接口的介绍(了解)
java.lang.reflect.Type
接口及其相关接口用于描述java中用到的所有类型,是Java的反射中很重要的组成部分。Type 是 Java 编程语言中所有类型的公共高级接口。它们包括原始类型、参数化类型、数组类型、类型变量和基本类型。
有很多场景下我们可以获得Type,比如:
- 当我们拿到一个Class,用
Class.getGenericInterfaces()
方法得到Type[],也就是这个类实现接口的Type类型列表。 - 当我们拿到一个Class,用
Class.getDeclaredFields()
方法得到Field[],也就是类的属性列表,然后用Field. getGenericType()
方法得到这个属性的Type类型。 - 当我们拿到一个Method,用
Method.getGenericParameterTypes()
方法获得Type[],也就是方法的参数类型列表。 - 当我们拿到一个Class,用
clazz.getGenericSuperclass()
这样就可以获取父类的泛型实参列表
2.3.1 Type的分类
Type接口包含了一个实现类(Class)和四个实现接口(TypeVariable, ParameterizedType, GenericArrayType, WildcardType),这四个接口都有自己的实现类,但这些实现类开发都不能直接使用,只能用接口。
- Class: 当需要描述的类型是普通Java类、数组、自定义类、 8种java基本类型 的时候, java会选择Class来作为这个Type的实现类,我们甚至可以直接把这个Type强行转换类型为Class。这些类基本都有一个特点:基本和泛型无关,其他4种Type的类型,基本都是泛型的各种形态。
- ParameterizedType: 当需要描述的类是泛型类时,比如List,Map等,不论代码里写没写具体的泛型,java会选择ParameterizedType接口做为Type的实现。ParameterizedType接口有getActualTypeArguments()方法,用于得到泛型的Type类型数组。
- GenericArrayType: 当需要描述的类型是泛型类的数组时,比如比如List[],Map[],type用GenericArrayType接口作为Type的实现。GenericArrayType接口有getGenericComponentType()方法,得到数组的组件类型的Type对象。
- WildcardType: 当需要描述的类型是泛型类,而且泛型类中的泛型被定义为(? extends xxx)或者(? super xxx)这种类型,比如List<? extends TestReflect>,这个类型首先将由ParameterizedType实现,当调用ParameterizedType的getActualTypeArguments()方法后得到的Type就由WildcardType实现。
示例代码获取泛型父类信息:
public class TestType { public static void main(String[] args) { //获取Son类的字节码对象 Class clazz = Son.class; //通过子类的字节码对象获取父类的泛型 Type type = clazz.getGenericSuperclass(); //强转 ParameterizedType parameterizedType = (ParameterizedType) type; //获取类型的泛型 Type[] typeArguments = parameterizedType.getActualTypeArguments(); for (Type typeArgument : typeArguments) { System.out.println(typeArgument); } } //泛型形参:<T,U> class Father<T,U>{ } //泛型实参:<String,Integer> class Son extends Father <String,Integer>{ } }
2.4 动态创建和操作任何类型的数组
在java.lang.reflect包下还提供了一个Array类,Array对象可以代表所有的数组。程序可以通过使用Array类来动态的创建数组,操作数组元素等。
Array类提供了如下几个方法:
public static Object newInstance(Class<?> componentType, int... dimensions):创建一个具有指定的组件类型和维度的新数组。
public static void setXxx(Object array,int index,xxx value):将array数组中[index]元素的值修改为value。此处的Xxx对应8种基本数据类型,如果该属性的类型是引用数据类型,则直接使用set(Object array,int index, Object value)方法。
public static xxx getXxx(Object array,int index,xxx value):将array数组中[index]元素的值返回。此处的Xxx对应8种基本数据类型,如果该属性的类型是引用数据类型,则直接使用get(Object array,int index)方法。
public class TestArray { public static void main(String[] args) { //使用反射操作数组 //1.使用反射创建一个String类型的数组,长度是5 String[] o = (String[])Array.newInstance(String.class, 5); //2.往数组中存入数据 for (int i = 0; i < 5; i++) { Array.set(o,i,"value"+i); } //使用Array获取数组中的元素 for (int i = 0; i < 5; i++) { System.out.println(Array.get(o,i)); } } }
3. 注解
3.1 学习目标
- 了解注解的概念
- 了解JDK提供的三种基本注解
- 掌握自定义注解
- 掌握元注解
- 掌握注解解析
3.2 内容讲解
3.2.1 什么是注解
注解英文是annotation,是一种代码级别的说明,和类 接口平级关系。相当于一种标记,在程序中加入注解就等于为程序打上某种标记,以后,javac编译器、开发工具和其他程序可以通过反射来了解你的类及各种元素上有无标记,看你的程序有什么标记,就去干相应的事,标记可以加在包、类,属性、方法,方法的参数以及局部变量上定义
3.2.2 注解的作用
(1)执行编译期的检查 例如:@Override
(2)分析代码(主要用途:替代配置文件); 用在框架里面, 注解开发
3.2.3 JDK提供的三个基本注解
@Override
:描述方法的重写.@SuppressWarnings
:压制警告.@Deprecated
:标记过时
3.2.4 自定义注解的语法
@Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation02 { }
/** * 注解的作用: * 1. 作为标记 * 2. 用于代替配置文件,存储一些配置信息 * * 使用注解存储数据,那么就得给注解添加注解属性 * 1. 语法: 属性类型 属性名(); * 2. 属性类型都可以是哪些类型? * 2.1 String * 2.2 8种基本数据类型 * 2.3 枚举类型 * 2.4 Class类型 * 2.5 注解类型 * 2.6 上述类型的数组类型 * * 3. 注解但凡定义了属性,那么我们在使用注解的时候,就要给注解的属性进行赋值 * * 4. 注解属性可以使用default进行赋默认值,赋了默认值的属性,我们在使用该注解的时候可以不进行赋值 * * 5. 如果注解属性是数组类型,但是赋值的时候该数组只有一个元素,那么可以省略数组的{} * * 6. 如果只有一个注解属性必须赋值,并且这个注解属性的属性名是value,那么赋值的时候可以省略 value= * * 元注解: * 1. Target 表示注解只能用在哪些位置 * 2. Retention 表示该注解保留到哪个阶段 */ @Target({ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation01 { String str() default "奥巴马"; int num() default 12; Color color() default Color.YELLOW; Class clazz() default UseAnnotation.class; MyAnnotation02 anno() default @MyAnnotation02; String[] value(); }
3.2.5 常用的元注解
(1)@Target
:定义该注解作用在什么上面(位置),默认注解可以在任何位置. 值为:ElementType
的枚举值
METHOD
:方法
TYPE
:类 接口
FIELD
:字段
CONSTRUCTOR
:构造方法声明
(2)@Retention
:定义该注解保留到那个代码阶段, 值为:RetentionPolicy
类型,默认只在源码阶段保留
SOURCE
:只在源码上保留(默认)
CLASS
:在源码和字节码上保留
RUNTIME
:在所有的阶段都保留
java (源码阶段) ----编译---> .class(字节码阶段) ----加载内存--> 运行(RUNTIME)
public class UseAnnotation { private int age; @MyAnnotation02 @MyAnnotation01(str = "hello", num = 1, color = Color.RED, clazz = UseAnnotation.class, anno = @MyAnnotation02, value = {"a","b"}) public void say(String name){ System.out.println(name); } public void study(){ System.out.println("努力学习..."); } }
/** * 注解解析 * 1. 目的: * 1.1 获取类、成员变量、成员方法、方法属性、构造函数等等上面的注解对象 * 1.2 获取注解对象的属性 * 1.3 判断某个类、成员变量、成员方法等等上面是否有某个注解 * 2. 注解解析的API,解析注解是通过AnnotatedElement接口进行注解解析的 * 2.1 T getAnnotation(Class<T>annotationType) 获取指定类型的注解 * 2.2 boolean isAnnotationPresent(Class<?extends Annotation> annotationType) 判断是否存在指定类型的注解 * * Method、Filed、Class、Constructor 都实现了AnnotatedElement接口 * */ public class TestAnnotation { public static void main(String[] args) throws Exception{ //注解解析: //目标1:判断UseAnnotation类的所有方法上,是否包含MyAnnotation02注解 //1.1 使用反射获取该类的所有方法 Class clazz = UseAnnotation.class; Method[] declaredMethods = clazz.getDeclaredMethods(); for (Method declaredMethod : declaredMethods){ boolean annotationPresent = declaredMethod.isAnnotationPresent(MyAnnotation02.class); System.out.println(declaredMethod.getName()+","+annotationPresent); } //目标2:获取setAnnotation类的say方法上的MyAnnotation01注解,并且拿到该注解 //2.1 使用反射获取say方法 Method say = clazz.getDeclaredMethod("say", String.class); //2.2 获取到say方法上的MyAnnotation01注解 MyAnnotation01 declaredAnnotation = say.getDeclaredAnnotation(MyAnnotation01.class); //注解的对象只能通过反射去获取,不能直接new System.out.println(declaredAnnotation.str()); System.out.println(declaredAnnotation.num()); System.out.println(declaredAnnotation.anno()); System.out.println(declaredAnnotation.clazz()); System.out.println(declaredAnnotation.color()); System.out.println(declaredAnnotation.value()); } }