声明:本文是我回顾javaSE所作笔记,如有错误还请指正。另外本文参考大量博文,数量众多,无法指明原文,如有冒犯,还请见谅!
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个这个类的Java.lang.Class对象,用来封装类在方法区类的对象。
类的加载的最终产品是位于堆区中的Class对象。 Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aVU2wTjC-1593694658223)(C:\Users\老代\AppData\Roaming\Typora\typora-user-images\image-20200618111946274.png)]
FIleOutputStream
public static void main(String[] args) { OutputStream out = null;//创建流 try { out = new FileOutputStream(new File("b.txt"));//选择源 out.write("hello word".getBytes());//操作流 out.flush();//刷新缓冲区 } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (out == null) { try { out.close();//关闭流 } catch (IOException e) { e.printStackTrace(); } } } }文件复制
public static void main(String[] args) { InputStream in = null; OutputStream out = null; try { in = new FileInputStream("b.txt");//选择要复制的文件 out = new FileOutputStream("copy.txt");//复制后的文件名 byte[] bytes = new byte[1024]; int readcount = 0; while ((readcount = in.read(bytes)) != -1){ out.write(bytes,0,readcount);//写入到文件中 } out.flush(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (in == null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } if (out == null) { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } }字节流: FileInputStream、 FileOutPutStram(重点)
字符流: FileReader、 FileWriter
缓冲流: BufferedInputStram、 BufferedOutputStram、 BufferedReader、 BufferedWriter
转换流: InputStramReader 、 OutPutStreamWriter
标准流: PrintStream、 PrintWriter
数据流: DataInputStream、 DataOutputStream
对象流:ObjectInputStream、 ObjectoutputtStream(重点)
程序:是为完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,静态对象。 进程:是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程,有它自身的产生,存在和消亡的过程。-------生命周期 线程:进程可进一步细化为线程,是一个程序内部的一条执行路径
# 线程与进程之间的关系与特点 进程与进程之间相互独立。线程是进程的最小执行单位,一个进程程可以有多个线程 线程之间的对内存和方法区共享,但是栈内存独立,一个线程一个栈 线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。串行:一个线程执行到底,相当于单线程。
并发:多个线程交替执行,抢占cpu的时间片,但是速度很快,在宏观角度看来就像是多个线程同时执行。
并行:多个线程在不同的cpu中同时执行。
# 并发与并行的区别: 并发严格的说【不是同时】执行多个线程,只是线程交替执行且速度很快,相当于同时执行。 而并行是同时执行多个线程,也就是多个cpu核心同时执行多个线程。线程分类:
1、守护线程(如垃圾回收线程,异常处理线程) 2、用户线程(如主线程) 若JVM中都是守护线程,当前JVM将退出。
方法一、继承于java.lang.Thread类(不推荐)
步骤:
继承Thread类重写run方法创建线程对象线程对象调用start()方法,开启一个栈 public class TestThread { public static void main(String[] args) { test test = new test(); test.start();//开启分支线程,开启一个栈空间 for (int i = 0; i < 1000; i++) { System.out.println("主线程"+i); } } } // class test extends Thread{ @Override public void run() { for (int i = 0; i < 1000; i++) { System.out.println("分支线程"+i); } } }方法二、实现Runnable接口
步骤:
新建类,实现Runnable接口实现类实现run方法创建实现类对象将此对象作为参数传递到Thread类中的构造器中,创建Thread类的对象通过Thread类的对象调用start() public class TestRunnable implements Runnable{ @Override public void run() { for (int i = 0; i < 1000; i++) { System.out.println("分支线程"+i); } } } class main{ public static void main(String[] args) { //创建实现类对象,将实现类对象作为参数传递到Thread类的构造器中,调用start方法 new Thread(new TestRunnable()).start(); for(int i = 0; i < 1000; i++) { System.out.println("主线程"+i); } } }方法三、实现callable接口方式(功能更加强大)
public class TestCallable implements Callable { @Override public Object call() throws Exception { int num = 0; for(int i = 0; i < 1000; i++) { if (i % 2 == 0){ System.out.println("分支线程:"+i); num += i; } } return num; } } class main2{ public static void main(String[] args) { //将Callable接口实现类作为参数传递到FutureTask FutureTask futureTask = new FutureTask(new TestCallable()); new Thread(futureTask).start();//启动分支线程 int num = 0; for(int i = 0; i < 1000; i++) { if (i % 2 == 0){ System.out.println("主线程:"+i); num += i; } } try { //通过FutureTask获取callable实现类实现call方法的返回值结果 System.out.println("分支线程结果:"+futureTask.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println("主线程结果:"+num); } } # 三种方式总结 由于java单继承的特性所以继承Thread类的方式就无法在进行扩展 相比于继承Threa类的后两种方式增加了程序的健壮性,且可以使用线程池 实现Runnable的方式比较常用,但是实现Callable的方式更加强大,call方法可以有返回值、方法可以抛出异常、支持泛型的返回值 但是需要注意的是当在获取线程的返回值时当前线程会进入到【阻塞状态】(必须等待需要结果的线程执行完毕才可获得结果,才能执行到当前 线程的代码),所以使用时注意场景,如果不需要返回值则优先考虑Runnable Thread类相关api作用start()启动当前线程、调用线程中的run方法currentThread()静态方法,返回执行当前代码的线程getName()获取当前线程的名字setName()设置当前线程的名字yield()主动释放当前线程的执行权join()在线程中插入执行另一个线程,该线程被阻塞,直到插入执行的线程完全执行完毕以后,该线程才继续执行下去stop()过时方法。当执行此方法时,强制结束当前线程。sleep(lmillitime)线程休眠一段时间isAlive()isAlive():判断当前线程是否存活扩展:使用线程池的方式批量使用线程
java线程池Executors常用api:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-610O4oXO-1593694658225)(C:\Users\老代\AppData\Roaming\Typora\typora-user-images\image-20200622184115484.png)]
步骤:
创建实现Runnable或者Callable接口的对象通过Executors创建线程池对象executorservice将创建好的实现了runnable接口类的对象放入executorService对象的execute方法中执行。关闭线程池 public class TestExecutorservice { public static void main(String[] args) { //创建一个可重用固定线程数为10的线程池 ExecutorService executorService = Executors.newFixedThreadPool(10); //将实现Runnable接口的实现类作为参数传递到excute方法中,执行线程 executorService.execute(new MyRunnable());//返回值为void适用于runnable //将实现Callable接口的实现类作为参数传递到submit方法中,执行线程 executorService.submit(new MyCallable());//有返回值,适用于callable executorService.shutdown();//关闭线程池 } } //通过实现Callable的方式实现多线程 class MyCallable implements Callable { @Override public Object call() throws Exception { for (int i=0;i<1000;i++){ System.out.println(Thread.currentThread().getName()+"【Callable】"+i); } return null; } } //通过实现Runnable的方式实现多线程 class MyRunnable implements Runnable{ @Override public void run() { for (int i=0;i<1000;i++){ System.out.println(Thread.currentThread().getName()+"【Runnable】"+i); } } }[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CBiIMXMo-1593694658227)(C:\Users\老代\AppData\Roaming\Typora\typora-user-images\image-20200623003306808.png)]
新建状态(New):新创建了一个线程对象。就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。即在就绪状态的进程除cup之外,其他可运行所需资源都已全部获得运行状态(Running):就绪状态的线程获取了CPU,抢夺到时间片,获得执行权阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。sleep:指让线程暂缓执行,等指定的时间再恢复执行
线程休眠会交出时间片,让CPU去执行其他的任务。调用sleep()方法让线程进入休眠状态后,sleep()方法并不会释放锁,即当前线程持有某个对象锁时,即使调用sleep()方法其他线程也无法访问这个对象。调用sleep()方法让线程从运行状态转换为阻塞状态;sleep()方法调用结束后,线程从阻塞状态转换为可执行状态。 # 唤醒线程睡眠 interrupt()方法(推荐) 该方法是使用异常处理机制。由于sleep方法会抛出异常,而interrupt方法会抛出sleep方法的异常类型,当sleep方法捕获到这次异 常也就唤醒了线程stop():强行终止(不推荐使用,已过时),注意该方法会终止此线程 由于stop方法的强行终止会使得数据存在丢失的问题,所以我们通常使用一个boolean类型的标识来控制线程的终止,这样更加安全可控
public class TestSleep implements Callable { boolean falg = true;//用来控制线程是否终止的变量 @Override public Object call() throws Exception { for (int i = 0; i < 100; i++) { if (falg){ Thread.sleep(1000); System.out.println(Thread.currentThread().getName()+"->>>"+i); }else return null; } return null; } } class main{ public static void main(String[] args) { TestSleep callable = new TestSleep(); FutureTask futureTask = new FutureTask(callable); new Thread(futureTask).start();//启动线程 try { Thread.sleep(1000*10);//10s钟后终止线程 callable.falg = false; } catch (InterruptedException e) { e.printStackTrace(); } } }yield():暂停当前正在执行的线程对象,并执行其他线程。
调用yield()方法让当前线程交出CPU权限,让CPU去执行其他线程。yield()方法和sleep()方法类似,不会释放锁,但yield()方法不能控制具体交出CPU的时间。yield()方法只能让拥有相同优先级的线程获取CPU执行的机会。使用yield()方法不会让线程进入阻塞状态,而是让线程从运行状态转换为就绪状态,只需要等待重新获取CPU执行的机会。join():指的是如果在主线程中调用该方法时就会让主线程休眠,让调用join()方法的线程先执行完毕后再开始执行主线程。
首先来看一个多线程场景下的抢票系统
public class Demo01 implements Runnable{ boolean flag = true; private int num = 10; @Override public void run() { while (flag){ if (num <= 0){ flag = false; break; }else { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"抢到了"+(num--)+"号票"); } } } } class main{ public static void main(String[] args) { Demo01 demo01 = new Demo01(); new Thread(demo01,"小明").start(); new Thread(demo01,"小白").start(); new Thread(demo01,"小代").start(); } } /** 小明抢到了3号票 小代抢到了3号票 小代抢到了2号票 小明抢到了0号票 小白抢到了1号票 花费的时间为:8062 花费的时间为:8062 花费的时间为:8062 **/其结果是不仅有可能多人抢到重复的一张票,且票数小于等于0时还未结束抢票,抢到0号票、负数票等与期待大大不同的结果,这就是多线程情况下的线程安全问题,解决这一问题我们就只能进行局部的线程同步,牺牲一部分性能来保证数据安全!
解决线程安全的方法
内置锁(synchronized)
内置锁也叫互斥锁,可以保证线程的原子性,当线程进入方法时,会自动获得一个锁,一旦锁被获得,其他线程必须等待获得锁的线程执行完代码释放锁,会降低程序的执行效率(相当于单线程执行)
方法一:同步方法
使用方法: 将同步代码块提取出一个方法,使用synchronized关键字进行修饰 对于runnable接口实现多线程,只需要将同步方法用synchronized修饰 而对于继承自Thread方式,需要将同步方法用static和synchronized修饰,因为对象不唯一(锁不唯一)
public class Demo02 implements Runnable{ private int num = 10;//票数 private boolean flag = true; @Override public void run() { long start = System.currentTimeMillis(); while (flag){ Buytickets(); try { Thread.sleep(2000);//如果不加以限制则一个线程所抢占的时间片足以将10张票全部抢到 } catch (InterruptedException e) { e.printStackTrace(); } } long end = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName()+"花费的时间为"+(end-start)); } public synchronized void Buytickets(){ if (num <= 0){ flag = false; return; }else { System.out.println(Thread.currentThread().getName()+"抢到了"+num-- +"号票"); } } public static void main(String[] args) { Demo02 demo02 = new Demo02(); new Thread(demo02,"小明").start(); new Thread(demo02,"小白").start(); new Thread(demo02,"小代").start(); } } /**结果 小白花费的时间为8012 小代花费的时间为8012 小明花费的时间为8012 **/ # 总结 1.同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。 2.非静态的同步方法,同步监视器是this 3.静态的同步方法,同步监视器是当前类本身。 4.可以看到使用了同步方法的抢票虽然保证了数据安全,但花费的时间是2000*10毫秒,其完全是异步的方式,牺牲了很多性能方法二:同步块
使用方法:
synchronized(同步监视器){ //需要被同步的代码 }
public class Demo03 implements Runnable { private int num = 10;//票数 @Override public void run() { long start = System.currentTimeMillis(); while (true) { synchronized (this) { if (num <= 0) { break; } else { System.out.println(Thread.currentThread().getName() + "抢到了" + num-- + "号票"); } } try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } long end = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + "花费的时间为" + (end - start)); } public static void main(String[] args) { Demo03 demo03 = new Demo03(); new Thread(demo03, "小明").start(); new Thread(demo03, "小白").start(); new Thread(demo03, "小代").start(); } } /** ······ 小白抢到了1号票 小代花费的时间为6009 小明花费的时间为6009 小白花费的时间为8010 **/ # 总结 1、同步监视器(俗称锁) 任何一个的对象都可以充当锁。(但是为了可读性一般设置英文成lock)当锁住以后只能有一个线程能进去(要 求:多个线程必须要共用 同一把锁,比如火车上的厕所,同一个标志表示有人) 2、Runable天生共享锁,而Thread中需要用static对象或者this关键字或者当前类来充当唯一锁 3、如何控制同步范围尽可能地小是线程安全的重点,因为解决线程安全就是将需要修改的共享数据进行线程同步,单线程必定相比多线程效率低下,所以控制同步范围尽可能地小来保证性能的损失幅度是线程安全的重点,也是多线程的重点方法三、JDK1.5新增的LOCK锁方法
步骤:
创建锁对象在run方法中需要保证线程安全的数据之前开启锁关闭锁 public class MyLock implements Runnable { private int num = 10;//票数 private ReentrantLock lock = null; public MyLock() { this.lock = new ReentrantLock();//实例化锁 } @Override public void run() { long start = System.currentTimeMillis(); while (true){ lock.lock(); try { if (num <= 0) { break; } else { System.out.println(Thread.currentThread().getName() + "抢到了" + num-- + "号票"); } }finally { lock.unlock();//防止死锁 } try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } long end = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName()+"花费的时间为" + (end - start)); } public static void main(String[] args) { MyLock lock = new MyLock(); new Thread(lock, "小白").start(); new Thread(lock, "小代").start(); new Thread(lock, "小黑").start(); } } /** ··· 小黑抢到了1号票 小代花费的时间为6013 小白花费的时间为6013 小黑花费的时间为8014 **/ # 总结 1、相比与内置锁sychronized LOck锁需要手动开启同步,手动关闭。synchronized在执行完相应的代码逻辑之后会自动释放同步监视器 (锁对象)相比较而言lock锁的方式更加灵活。 在jdk1.5之前synchonized的效率相比lock锁十分低下,但jdk6后官方对synchonized进行了性能优化,官方更加推荐使用 synchonizde块。 2、因为lock要手动释放锁,所以如果发生异常时就不会释放锁。因此使用lock必须在try/catch块中进行。以便在finally块中保证锁的 释放 3、trylock()方法:尝试去获取锁,如果获取成功返回true,如果获取失败(锁对象被其他线程所持有)返回false。也就是说这个方法无 论如何都会立即返回,在拿不到锁时不会一直等待。如果程序因为IO或者其他原因进入到阻塞状态,使用synchonized会一直等待,会 大大影响效率所以相比于synchonized lock锁的方式在某些场景下是更加灵活的!重入锁和不可重入锁
重入锁:即获得锁的线程可以进入它拥有的锁的同步代码块 不可重入锁:即获得锁的线程,在方法中尝试再次获得锁时,获取不到进入阻塞状态
死锁产生的原因:同步中嵌套同步,同步锁是一个重入锁,就很有可能发生死锁
死锁的解决办法
减少同步共享变量采用专门的算法,多个线程之间规定先后执行的顺序,规避死锁问题减少锁的嵌套[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xdh3K0pq-1593694658229)(C:\Users\老代\AppData\Roaming\Typora\typora-user-images\image-20200625003446974.png)]
线程通信的方法多用在生产者和消费者的模式中,下面将模拟工厂生产馒头,工厂和消费者都在不停的生产和消费,只不过当工厂生产100个馒头时停产,当消费者消费完100个馒头时停止消费!
public class Test { public static void main(String[] args) { List list = new ArrayList(); Producter producter = new Producter(list); Customer customer = new Customer(list); new Thread(producter,"生产者").start(); new Thread(customer,"消费者").start(); } } class Producter implements Runnable{ private List list = null; public Producter(List list) { this.list = list; } @Override public void run() { while (true){ synchronized (list){ if (list.size() >= 100){//仓库满了,停止生产 try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } for (int i = 1; i <=100 ; i++) { list.add(i); System.out.println("生产者生产了第"+i+"个馒头"); } //通知消费者消费 list.notify(); } } } } class Customer implements Runnable{ private List list = null; public Customer(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 = 100; i > 0; i--) { System.out.println(Thread.currentThread().getName()+"消费了第"+list.get(i-1)+"个馒头"); list.remove(i-1); } list.notify(); } } } }编译期:编译期是指编译器将源代码翻译为机器能识别的代码,java为编译为jvm认识的字节码文件(.class文件)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DESJkxUb-1593694658230)(C:\Users\老代\AppData\Roaming\Typora\typora-user-images\image-20200630200448001.png)]
# 在编译期,将java代码翻译为字节码文件的过程经过了四个步骤,词法分析,语法分析,语义分析,代码生成四个步骤。 # 编译期只是做了一些翻译功能,并没有把代码放在内存中运行起来,而只是把代码当成文本进行操作,比如语法检查. # 编译成功后的结果就是生成字节码文件**运行期:**java虚拟机【分配内存】,解释执行字节码文件。类加载就是运行期的开始
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NXsc7TSs-1593694658230)(C:\Users\老代\AppData\Roaming\Typora\typora-user-images\image-20200630200758168.png)]
关于编译期和运行期的具体分析请看
反射是Java的特征之一,是一种间接操作目标对象的机制,核心是JVM在运行的时候才【动态加载类】,并且对于任意一个类,都能够知道这个类的所有属性和方法,调用方法/访问属性,不需要提前在编译期知道运行的对象是谁,他允许运行中的Java程序获取类的信息,并且可以操作类或对象内部属性。程序中对象的类型一般都是在编译期就确定下来的,而当我们的程序在运行时,可能需要动态的加载一些类,这些类因为之前用不到,所以没有加载到jvm,这时,使用Java反射机制可以在运行期动态的创建对象并调用其属性,它是在运行时根据需要才加载。
# 首先明确反射机制是运行期间的 # 简单说,反射机制是指在运行期间可以操作字节码文件。在java中,只要给定类的名字,那么就可以通过反射机制来获得类的所有信息。 # java虽然不是动态语言,但是java因为反射机制所以拥有着动态语言的部分特性[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bVuXRmFh-1593694658231)(C:\Users\老代\AppData\Roaming\Typora\typora-user-images\image-20200630202343473.png)]
方式一:通过Object类的getClass方法获得 Class c = 对象.getClass()
方式二:任何数据类型(包括基本的数据类型)都有一个“静态”的class属性 Class c = 类名.class
方式三:通过class类的静态方法:Class.forName(String className)(最常用) Class c = Class.forName(类的全限定名称)
注意:Class.forName()会导致类加载,类初始化的过程静态代码块执行。所以如果你只想执行一个类的静态代码块,其他一律不执行可以使用Class.forName()。如在【jdbc中注册驱动】就是使用了这个机制
public static void main(String[] args) { //获取Class对象的第一种方式:对象.getClass() Class a = new String().getClass(); //获取Class对象的第一种方式:类名.class Class b = String.class; //获取Class对象的第一种方式:forName(string name) try { Class c = Class.forName("java.lang.String"); System.out.println(a==b); System.out.println(a==c); /** * 结果为: true true * 分析:在运行期间,一个类只有一个Class对象,所以三个对象的内存地址相同 */ } catch (ClassNotFoundException e) { e.printStackTrace(); } }方法一:Class对象.newInstance()
public class TestGetBean { public static void main(String[] args) { try { //获取Student的Class对象 Class c = Class.forName("反射机制.通过反射机制实例化对象.Studnet"); //创建实例 Class对象.newInstance() Object student = c.newInstance();//注意这个方法是通过无参构造器实例化的对象 System.out.println(student);//反射机制.通过反射机制实例化对象.Studnet@b4c966a } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } } } class Studnet{ } # 注意通过这种方式实例化对象底层是通过无参构造器实例化的对象,如果没有无参构造器则会抛出异常 # 与直接new对象的方式相比反射机制实例化对象更具有灵活性,如可以通过属性配置文件动态的创建对象,想要创建不同的对象只需更改properties的类名即可通过反射机制实例化对象的灵活性测试
import java.io.FileReader; import java.util.Properties; public class Test { public static void main(String[] args) throws Exception { FileReader reader = new FileReader("Bean.properties"); Properties properties = new Properties(); properties.load(reader);//加载属性配置文件 reader.close(); String className = properties.getProperty("name"); Class c = Class.forName(className); Object obj = c.newInstance();//实例化对象 System.out.println(obj); } } /** * 只要更改属性配置文件的name属性,就可动态的创建不同的对象 *如name=name=java.util.Date 结果为Tue Jun 30 21:32:08 CST 2020 *如name=name=name=java.lang.Object 结果为java.lang.Object@7291c18f */方法二:Class对象.getDeclaredConstructor().newInstance()
public class Test3 { public static void main(String[] args) throws Exception { Class c = Class.forName("反射机制.通过反射机制实例化对象.Teacher"); //通过无参构造器实例化对象 Object techer = c.getDeclaredConstructor().newInstance();//通过无参构造器实例化对象 Object o = c.getDeclaredConstructor(int.class).newInstance(10);//通过有参构造器实例化对象:10 } } class Teacher{ private int id; public Teacher() { System.out.println("通过无参构造器实例化对象"); } public Teacher(int id) { this.id = id; System.out.println("通过有参构造器实例化对象:"+id); } }Field类的相关方法
作用方法返回该Fileld表示的字段在指定对象上的值get(Object obj)返回一个AnnotatedType对象,它表示使用一个类型来指定此Field所表示的字段的声明类型getAnnotatedType()将指定的对象参数中由此 Field对象表示的字段设置为指定的新值set(Object obj, Object value)Method类的相关方法
作用方法获取方法的返回值getGenericReturnType()返回一个 Type类型的数组, Type以声明顺序表示由该对象表示的可执行文件的形式参数类型getGenericParameterTypes()获取方法名getName()在具有指定参数的指定对象上调用此方法invoke(Object obj, Object… args)反射机制是java弱动态性的体现,在平时的开发中我们使用反射机制是很少的,但是我们要理解这种机制,这对于我们以后的了解各类框架的底层源码是很有帮助的。
注解是一种能被添加到java代码中的元数据,类、方法、变量、参数和包都可以用注解来修饰。注解对于它所修饰的代码并没有直接的影响。(翻译自官方描述)
# 通过官方描述可得的结论 1、注解是一种元数据形式。即注解是属于java的一种数据类型,和类、接口、数组、枚举类似。 2、注解用来修饰,类、方法、变量、参数、包、注解 3、注解不会对所修饰的代码产生【直接】的影响。很抱歉通过官方的描述我们还是无法理解什么是注解。反而会一头雾水陷入新的迷惑,什么是元数据?注解到底是什么?注解的作用 ?
# 元数据:简单来说元数据就是用来描述数据的数据。 任何文件系统中的数据分为数据和元数据。数据是指普通文件中的实际数据,而元数据指用来描述一个【文件的特征】的系统数据,诸如访问权限、文件拥有者以及文件数据块的分布信息(inode...)等等。在集群文件系统中,分布信息包括文件在磁盘上的位置以及磁盘在集群中的位置。用户需要操作一个文件必须首先得到它的元数据,才能定位到文件的位置并且得到文件的内容或相关属性。在了解元数据的特征后相信您已经对注解有了新的认识,那么我们就可大胆的对注解作一个自己的定义
注解:注解就是对其所修饰的类、方法、变量、参数、注解等的说明数据,所以注解本质上还是元数据。简单来说注解注就是代码里的特殊【标记】,这些标记可以在编译,类加载,运行时被读取,并执行相应的处理。通过注解开发人员可以在不改变原有代码和逻辑的情况下在源代码中嵌入补充信息。
# 与注解类似的还有xml(可扩展的标记语言) 注解:是一种分散式的元数据,与源代码紧绑定 xml:是一种集中式的元数据,与源代码无绑定 注解与xml最大的区别就是是否与源码绑定,因此两者的选择上可以从两个角度来看:分散还是集中,源代码绑定/无绑定.选择合适的适用场景才能发挥各自最大的优势 # 注解与xml的核心优势 注解:配置简单,维护方便(我们找到类,就相当于找到了对应的配置) xml:修改时,不用改源码。不涉及重新编译和部署.相对来说xml的功能更加强大 注解与xml的选择有很多争论,再比不作细谈。但是以目前的趋势来看逐渐形成以注解为主xml为辅的局势。但是由于xml更加强大的功能、相比于注解的特有优点,注解不会完全取代xml。两者的使用实际有特定的场景,如xml在Mapper映射文件中的使用jdk注解:@Override 表示当前方法覆盖了父类的方法、@Deprecated 表示方法已经过时,方法上有横线,使用时会有警告。
元注解:元注解的作用就是负责注解其他注解 Java5.0定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。Java5.0定义的元注解:
@Target@Retention@Documented@Inherited@Target:规定注解的作用范围
作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)
使用方法:@TargetI(ElementType.作用范围<枚举类型>)
1.CONSTRUCTOR:用于描述构造器 2.FIELD:用于描述域 3.LOCAL_VARIABLE:用于描述局部变量 4.METHOD:用于描述方法 5.PACKAGE:用于描述包 6.PARAMETER:用于描述参数 7.TYPE:用于描述类、接口(包括注解类型) 或enum声明
# 首先先了解一下自定义注解的简单定义 [修饰符列表] @interface 注解名{ 定义体 } 关于定义体的内容暂时不作深究,稍后再谈 import java.lang.annotation.ElementType; import java.lang.annotation.Target; /** *测试元注解:@Target:描述注解的作用域范围 */ @Target({ElementType.METHOD,ElementType.TYPE})//规定注解的使用范围 public @interface Token {//自定义注解 } @Token//作用在类上 class test{ @Token//报错,无法作用在类的成员变量上 private String name; @Token//作用在方法上 public String getName(){ @Token//报错,无法作用在局部变量上 String name; return this.name; } }@Retention:注解的保留策略
作用:表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)
取值(RetentionPoicy)有: 1.SOURCE:在源文件中有效(即java源文件保留) 2.CLASS:在class文件中有效(即class保留) 3.RUNTIME:在运行时有效(即运行时保留,**注意:**只有注解信息在运行时保留,我们才能在运行时通过反射读取到注解信息)
注解的保留策略只能三选一
@Target(ElementType.FIELD)//规定此注解的作用范围只能描述域 @Retention(RetentionPolicy.RUNTIME)//保留策略为 public @interface Column { }定义注解格式: [修饰符列表] @interface 注解名 {定义体}
使用@interface自定义注解时,自动继承了java.lang.annotation.Annotation接口,由编译程序自动完成其他细节。在定义注解时,不能继承其他的注解或接口。@interface用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。可以通过default来声明参数的默认值。
注解的元素(属性)
格式:修饰符 元素类型 属性名();
注解元素的可支持数据类型:
1.所有基本数据类型(int,float,boolean,byte,double,char,long,short) 2.String类型 3.Class类型 4.enum类型 5.Annotation类型 6.以上所有类型的数组
# 注解元素的说明 第一:修饰符只能用public或默认(default)这两个访问权修饰 第二:元素类型只能使用以上所规定的 第三:元素定义时可以指定默认值,定义为修饰符 元素类型 元素名() default value 因为注解元素元素必须有确定的值,要么在定义注解的默认值中指定,要么在使用注解时指定,非基本类型的注解元素的值不可为null。所以我们在定义时指定元素的默认值为一些特殊值,如空字符串或者0来表示。 # 注解的使用 1、@注解名(属性名=属性值,属性名=属性值···) 2、在使用注解时如果注解元素没有声明默认值,那么我们在使用时必须指定其元素值 3、如果注解本本身只有一个属性,且命名为value,那么在使用注解的时候可以直接使用:@注解名(注解值),其等效于:@注 解名(value = 注解值)。 4、如果注解元素类型是一个数组,使用时如果只需一个值,则{}可以省略,@注解名(类型名 = 类型值),它和标准写法:@注解名(类型名 = {v1,v2})等效!如果只定义注解、标注了注解而不去处理,那么注解是毫无作用的,因为注解不会对代码造成直接影响。所以紧接着应该去处理这个注解
下面将通过反射机制去处理注解
让我们梳理一下步骤:
首先应该先定义一个注解然后去定义注解处理器,声明这个注解的作用使用注解 /** * 声明一个注解该注解只能作用于方法、类、接口(包括注解类型) 或enum声明,用于检查权限 * 如果使用者在方法或者类上声明了注解,那么对其进行权限检查 * 检查方法是:如果注解的属性:subject 的值为"老代",那么就进行操作,否则报错 */ @Target({ElementType.METHOD,ElementType.TYPE})//只能作用于类和方法上 @Retention(RetentionPolicy.RUNTIME)//运行时保留,可以被反射 public @interface CheckPermissions { String subject(); } /** * 处理注解 */ import java.lang.reflect.Method; public class TestAnnotation { public static void main(String[] args) throws Exception { Class c = Class.forName("注解.处理注解.EmpController");//获取类 //判断类是否有@CheckPermissions注解 if (c.isAnnotationPresent(CheckPermissions.class)){ //获取注解对象 CheckPermissions annotation = (CheckPermissions) c.getAnnotation(CheckPermissions.class); String subject = annotation.subject();//获取注解的属性 if (subject.equals("老代")){ System.out.println("执行操作"); }else { throw new NoRightsExection("无权操作,请登录"); } } Method[] methods = c.getMethods();//获取类的方法 for (Method method : methods) { //判断方法上是否有@CheckPermissions注解 if (method.isAnnotationPresent(CheckPermissions.class)){ //获取注解对象 CheckPermissions annotation = method.getAnnotation(CheckPermissions.class); String subject = annotation.subject();//获取注解的@CheckPermissions的属性subject if (subject.equals("老代")){ System.out.println("执行操作"); break; }else { throw new NoRightsExection("无权操作,请登录"); } } } } } /** * 使用注解 */ @CheckPermissions(subject = "老代") class EmpController{ @CheckPermissions(subject = "小白") public Object insertEmp(){ System.out.println("添加员工"); return null; } }可能我上方的例子并不恰当,但是阅读之后你应该了解到怎么去处理注解。另外需要声明的时注解的保存策略不同,处理方法也并不相同,如我们熟悉的@Override注解只在java文件中保留,所以并不能通过注解去处理他。而@Override的作用仅仅只是告诉编译器着是个重写父类的方法,仅能作用于方法上,如果使用不当会报错,起一个标识作用。至于@Override的处理逻辑感兴趣你可以去 查阅相关文档
注解和反射相同,是大量使用在框架中的。并且在我们以后的开发中还是会使用得到的。如我上方的检查权限注解,可能就会在你开发中判断请求权限时使用。所以我们要能看的懂注解、能使用自定义注解、能解释注解。掌握了反射和注解对于我们读懂框架源代码是十分重要的,是我们必须掌握的技能!
符注解知识体系图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H5JfgEWl-1593694658232)(C:\Users\老代\AppData\Roaming\Typora\typora-user-images\image-20200702182059479.png)]
