java基础篇
声明:
该文档参考于b站老杜java视频。视频地址: https://www.bilibili.com/video/BV1Rx411876f/?spm_id_from=333.337.search-card.all.click&vd_source=bc31f1a530d129ce331819489ac66472
文中图片参考于视频,网络,如有侵权,联系删除qq:1945478169
5万字记录
final关键字
1.final表示最终的,不可变的
2.final可以修饰变量、 方法 、类等
3.final管不了能不能调用问题,final修饰的就是不能修改
如果不希望别人对一个类进行扩展,就是不能继承这个类,那么就需要给这个类加上final关键字
final修饰的方法不能被覆盖,但可以被继承表示这个方法想一直传下去的时候不想子类改变
final修饰的变量只能赋值一次,一旦赋值之后就不能修改了(局部变量没有初始值)
这样可以
如果final修饰的是一个引用的变量,则指向对象的内存地址不能变,但是指向对象的属性是可以修改的
final修饰的实例变量不会赋默认值!!!必须手动赋值,如果定义的时候没有赋值的时候,那么无论哪一个构造方法都要给final修饰的实例变量赋值,不能出现没有给final修饰实例变量的构造方法,要确保final修饰的实例变量一定要附上值!!!子类继承这个类也是要确保final修饰的实例变量附上值
这样就不行,因为无参构造没有给name赋值,final修饰的实例变量没有set方法
一般final与static一起使用 static与final一起修饰的变量称之为常量,变量名大写,每一个单词之间使用下划线分割
小总结
Animal类与Cat不在同一包下,move方法使用了protected修饰,使用多态调用move方法却调用不到
这是因为Cat中的move方法被protected修饰了,Animal类不跟Cat类再同一包下,Animal不是Cat的子类所以就调用不到
抽象类
抽象类(abstract修饰)无法实例化,类与类之间再进一步抽象形成了抽象类 抽象类也是引用数据类型
抽象类也可以继承抽象类,abstract不能与final、private联合使用
语法:
抽象类有构造方法,是给子类使用的
抽象类关联到一个概念:抽象方法 ,抽象方法表示没有实现的方法 ,没有方法体的方法,留给子类重写,抽象类当中也可以没有抽象方法,也可以有非抽象方法,如果一个类当中有抽象方法的话那么这个类一定是抽象类,一个抽象类继承另外一个抽象类的话也可以不重写抽象父类的方法,抽象下去,但一个非抽象类继承一个抽象类就必须重写抽象类中的方法!!!抽象类与实现类之间也可以使用多态机制
抽象类小总结
java中没有方法体的一定是抽象方法吗???错误,因为java底层还调用了C++代码,也是没有方法体
接口
接口是完全抽象的,也可以说接口是特殊的抽象类,接口里面所有方法都是抽象的 ,接口里面就两部分内容,一部分是常量,一部分是抽象方法,接口里面的抽象方法默认都是public abstract类型的,接口中的所有东西都是public修饰的,接口中的常量默认是 public static final 修饰的,默认就有。接口名.常量可以直接访问接口常量
定义:[修饰符列表] interface 接口名{} ,接口生成的也是class文件
一个抽象类实现(implement)接口也可以不覆盖里面的方法,但是一个非抽象类实现接口就必须覆盖接口里面全部方法 ,一个类可以实现多个接口,这种机制实际上弥补了java单继承的缺陷。接口可以继承别的接口,可以继承多个接口,接口继承接口会把接口里面所有抽象方法和常量继承过来。接口可以使用多态机制。类实现接口的方法可以重载
这种编译能通过,但运行出错,要使用instanceof判断
D实现了A,B,C三个接口,可以使用多态,因为D又实现了A,B,C三个接口当中的所有方法,则多态可以强转
extends写在前面 ,implements写在后面
is a(继承)、has a(关联)、like a(实现)
is a:
Cat is a Animal(猫是一个动物)
凡是能够满足is a的表示“继承关系”
A extends B
has a:
I has a Pen(我有一支笔)
凡是能够满足has a关系的表示“关联关系”
关联关系通常以“属性”的形式存在。
A{
B b;
}
like a:
Cooker like a FoodMenu(厨师像一个菜单一样)
凡是能够满足like a关系的表示“实现关系”
实现关系通常是:类实现接口。
A implements B
抽象类和接口有什么区别?
在这里我们只说一下抽象类和接口在语法上的区别。
至于以后抽象类和接口应该怎么进行选择,通过后面的项目去体会/学习。
抽象类是半抽象的。
接口是完全抽象的。
抽象类中有构造方法。
接口中没有构造方法。
接口和接口之间支持多继承。
类和类之间只能单继承。
一个类可以同时实现多个接口。
一个抽象类只能继承一个类(单继承)。
接口中只允许出现常量和抽象方法。
这里先透露一个信息:
以后接口使用的比抽象类多。一般抽象类使用的还是少。
接口一般都是对“行为”的抽象。
接口有什么用?扩展性好。可插拔。
分析:中午去饭馆吃饭,这个过程中有接口吗?
接口是抽象的。 菜单是一个接口。(菜单上有一个抽象的照片:西红柿炒鸡蛋)
谁面向接口调用?(顾客面向菜单点菜,调用接口。)
谁负责实现这个接口?(后台的厨师负责把西红柿鸡蛋做好!是接口的实现者。)
这个接口有什么用呢?
这个饭馆的“菜单”,让“顾客”和“后厨”解耦合了。
顾客不用找后厨,后厨不用找顾客,他们之间完全依靠这个抽象的菜单沟通。
package与import
package是一个关键字,后面加包名 package只允许出现在java源代码的第一行
包名命名规范:公司域名倒叙+项目名+模块名+功能名
import导包,java.lang包是自动导入进来的,String就在lang包下面,其余的包一律需要导入进来,在同一包下面的也不需要导入
访问控制权限
private私有的 protected受保护的 public 公开的 还有默认不写
private表示私有的,只能在本类中使用 public 表示公开在,在任何位置都能访问
访问控制权限可以修饰实例变量(4个都可以使用) 方法(静态方法也可以)(4个都可以使用) 类(只能使用public和默认修饰) 接口(跟类一样)等
Object类
toString方法
Object默认实现是类名+“@”+hashcode值,不重写hashcode值,创建两个对象的hashcode值不一样,重写了就一样
通过调用这一个方法可以将java对象转换成字符串表示形式,建议所有类都重写toString方法
equals方法
equals的源代码:
基本数据类型使用==
判断是否相等,java对象使用equals方法判断是否相等,而Object中的equals源码采用的就是==
,所以不够用
基本数据类型里面保存的就是值,但引用数据类型里面保存的是对象的地址,==
比较的是变量里面保存的东西
这个时候就要重写equals方法
这个时候就可以判断两个对象是否相等了 重写后参数不能写User u 因为父类Object中就是Object o
如果一个类A当中有引用属性的话,那么这个引用类型指向的类B也要重写equals方法,那么A重写equals方法的时候就可以直接调用子类的equals方法,或者在A类equals方法中判断传过来的B的各个属性是否相等
java中基本数据类型使用==判断相等,java中引用数据类型统一使用equals判断相等
String重写了toString与equals方法
String s1="123"
创建的对象是在方法区中的字符串常量池中,又创建的一个与s1值相等的s字符串对象,那么为了节省空间,s也指向s1创建的对象,所以两者是相等的,而new出来的字符串一定会在堆内存中创建对象
String str1="ABCD";String str2=new String("ABCD");
创建str1对象发现方法区内存里面没有就会创建一个,当使用new再创建一个值与str1相等的str2字符串对象的时候,就回去方法区中去找,找到了就在堆内存中创建一个字符串对象指向方法区中与str2值一样的str1创建的对象。如果str2创建的时候没在方法区中找到,那就创建两个对象,一个在方法区中,一个在堆内存当中,堆内存中新创建的指向在方法区中新创建的
String str1 = “ABCD”;最多创建一个String对象,最少不创建String对象。如果常量池中,已经存在“ABCD”,那么str1直接引用,此时不创建String对象,否则,先在常量池先创建“ABCD”内存空间,再引用。
String str2 = new string(“ABCD”);最多创建两个String对象,至少创建一个String对象。new关键字:绝对会在堆空间创建内存区域。
空值问题:
String对象的空值:
1):表示引用为空(null): String str1 = null; 没有初始化,没有分配内存空间
2):字符序列为空字符串:string str2 = “”; 已经初始化,分配内存空间,不过没有内容。
finalize方法
这个方法是protected修饰的 Object中的源码是
这个方法不需要程序员就手动调用,JVM的垃圾回收器会负责调用这个方法
当一个java对象即将被垃圾回收机制回收的时候,垃圾回收器负责调用
如果希望在对象被JVM回收的时候,执行一段代码就是用这个方法
当一个对象变成垃圾的时候,gc不一定会回收,等垃圾到达一定数量的时候gc才会启动System.gc()
会建议gc启动,但gc不一定启动,finalize()方法执行的时间不一定是对象变成垃圾的时间
内部类
在类的内部定义了一个新的类,称之为内部类
分为 1.静态内部类 :类似于静态变量 2.实例内部类:类似于实例变量 3.局部内部类:类似于局部变量
内部类可读性不高,能不能就不用
匿名内部类
匿名内部类是局部内部类的一种,因为这个类没有名字而得名
调用Teacher的mySum方法的时候需要穿一个Computer接口的实现类,我们还要定义一个Computer接口的实现类,假如我们懒得写!那就这样写
这个类没有名字,所以叫内部内部类,但这样写可读性不高,而且这个类也不能重复使用,不建议这样写,这样写逼格高一点罢了。。。
clone()方法
clone()方法在Object中,如果子类调用clone方法就必须实现Cloneable接口
Person p = new Person(23, "zhang");
Person p1 = p;
System.out.println(p);
System.out.println(p1);
我们看到创建了一个Person对象,创建的地址给了p ,又给p1赋值p,这两个指向的同一对象,赋值的只是Person对象的地址,打印出来的地址相同
如果想给pl也在堆内存当中创建一个与p属性全部相同的对象就要用到clone()方法
Person p = new Person(23, "zhang");
Person p1 = (Person) p.clone();
System.out.println(p);
System.out.println(p1);
这样打印出来的地址就不相同了
clone方法默认是浅拷贝的 什么意思?就是如果一个对象中含有其他对象的引用的话,克隆出来的新对象里的引用类型变量地址指向的还是原对象内引用类型地址,可以实现不完全的深拷贝,即原对象内所有引用类型变量都实现Cloneable接口。原对象重写clone方法时,这些引用类型变量也调一次clone方法。彻底深克隆是几乎不可能实现的
数组
数组变量的地址实际上指向的是数组第一个元素的地址,数组之间各个元素的存储地址是连续的,所以就可以通过第一个元素地址找到其他元素,数组的元素有默认值,数组中的元素如果是引用类型的话,往里面增加元素可以增加该引用类型的子类
初始化一维数组分为静态初始化一维数组和动态初始化一维数组
静态:int[] array={1,2,2};确定数组存储哪一些具体的数据
动态 int[] array =new int[5];这样写这里的5表示的是数组的长度 ,也可以使用int[] array1=new int[]{0,0,0,0,0};不确定数组存储哪一些具体的数据,先写一个固定长度
如果调用的方法的参数是一个数组的时候
这样传入静态数组
main方法
main的String[] args数组参数,调用主方法的时候JVM传入数组的长度为0,那这个main方法的参数有什么用???
其实这个数组是留给用户的,用户可以在控制台上输入参数,传给main方法中的String数组
例如 java demo5 abc def xyz
,JVM会自动将后面三个参数传给main方法中的数组
到底有什么用 例如:再运行系统的时候 需要输入用户名和密码 那么运行这个main方法的时候就要传入用户名以及密码,并判断是否正确,输入正确的用户名和密码才能使用
数组的扩容:
java中数组长度一旦确定就不可变只可以扩容
扩容数组实际上先新建一个大容量的数组,然后将之前小容量的数组中的数据一个个拷贝到新的大数组当中,效率不高
数组的拷贝
第一个参数是原始数组,第二个参数是拷贝原数组起始位置,第三个参数是拷贝后的数组,第四个拷贝数组的起始位置,第五个是拷贝长度,拷贝后原数组不变,引用数据类型也可以拷贝,引用数据类型拷贝的是对象的地址
二维数组
二维数组实际上就是一个一维数组,这个一维数组里面的每个元素还是一维数组,二维数组可以不指定每个元素数组的长度如上图
但这样写就不行
还可以这样写跟一维数组一样
二维数组的长度实际上是行数,也就是元素个数,元素是一维数组
二分查找
二分查找是建立在排序号的数组基础之上进行查找
Arrays工具类
这个工具类在java.util.Arrays下,我们开发的时候需要参考ApI文档,不要死记硬背
主要使用排序和二分查找
String详细
System.out.println("hello world");
这个字面量实际上还是在底层有一个String对象
1.java中所有使用 " " 括起来的都是String对象
2.java中所有双引号括起来的,都是不可变的,也就是说"abc"从出生到死亡都是不可变的
3.java中使用双引号括起来的都直接存储在方法区中的字符串常量池中,因为字符串使用特别频繁
String s1="abcdef";String s2="abcdef"+"xy";
第一句代码会在方法区中创建一个"abcdef"字符串对象使用s1指向,第二句会再在方法区中创建一个"xy"字符串对象,运算完成之后再用s2指向创建一个"abcdefxy"字符串对象,内存图如下
如果再加一句String s3=new String("xy");
new出来的一定会在堆内存当中创建一个对象,但是新创建的这个对象值的话会在方法区中找与创建相等的值的字符串对象,若果找到了,就在堆内存当中创建一个对象,对象当中指向在方法区中的字符串对象,JVM图如下
如果没有找到的话,就在方法区当中创建一个值与新创建对象值一样的字符串对象,并且在堆内存当中也创建一个指向这新在字符串常量池创建对象的对象
String s1="hello";String s2="hello";System.out.println(s1==s2);
输出true
第一句执行在方法区内存当中创建一个对象用s1指向,再创建一个值与s1一样的时候,就把s1指向的对象地址传给s2
String x=new String("xyz");String y=new String("xyz");System.out.println(x==y);
输出结果是false
第一句创建字符串对象,在方法区中字符串常量池当中没有发现"xyz",就在字符串常量池当中创建一个,并且在堆内存当中创建一个指向这个新在方法区中创建值为"xzy"的对象对象,并且把这个在堆内存当中新创建的对象地址赋值给x。第二句创建对象的时候发现方法区当中有"xyz"对象,则就在堆内存当中创建一个指向这个"xyz"对象的对象
以下代码一共创建了三个对象
再看
按理来说应该使TRUE,但为什么是false?
因为java只有在编译阶段才会把字符串对象放在字符串常量池当中,运行阶段不会,所以代码第6行创建s3的时候要在运行阶段,那么就会在堆内存当中创建,所以为FALSE
反观以下就是TRUE
String 的常用构造方法
直接传入一个byte数组,后面还可以加起始位置和长度
直接传入char数组,后面还可以加起始位置和长度
substring方法
截取字符串的方法可以就加一个参数,表示从这个下标开始一直截取到最后,也可以再增加一个截取到哪里结束,但不包括指定结束位置的那一个元素
String中只有一个静态方法valueof
将不是字符串的数据转换成为字符串,对象也可以转,底层调用的是对象的toString方法
打印方法,底层就调用了String.valueof()方法,所有打印出来的都是字符串类型
所以为什么直接输出引用就会直接调用toString方法,因为底层会使用String.valueof()方法,这个方法的底层又使用了对象的toString方法。
StringBuffer与StringBuilder
拼接字符串使用+号有什么问题??
因为java中的字符串是不可能改变的,每次拼接都会产生新的字符串,这样就会占用大量方法区内存,造成空间的资源浪费
StringBuffer底层也是字符数组,String的字符数组使用了final修饰了,所以在方法区中创建的String对象中的数组是不能能变的,String在字符串拼接的时候就会创建新的对象。而StringBuffer的数组没有使用final修饰所以StringBuffer的数组是可以变的 ,而StringBuffer使用的是数组扩容机制,不会产生新的对象StringBuffer默认初始化容量是16
也可以在创建对象的时候传入初始化容量
StringBuilder跟StringBuffer差不多,只不过StringBuffer中的方法都是使用synchronized修饰的说明StringBuffer在多线程环境下是安全的,StringBuilder是非线程安全的
包装类
java中为8种基本数据类型有对应准备了8种包装类型,8种包装类属于引用数据类型,父类都是Object
包装类存在的意义?
这样居然可以,int可以自动转换成Integer(自动装箱),这个类的父类又是Number(是一个抽象类),Number又是Object的子类,所以就可以
八种基本数据类型都是什么?(都在lang 包下面)
byte --> Byte(父类是Number)
short --> Short(父类是Number)
int --> Integer(父类是Number)
long --> Long(父类是Number)
floot --> Floot(父类是Number)
double --> Double(父类是Number)
boolean --> Boolean(父类是Object)
char --> Character(父类是Object)
Number有例如floatValue()的方法
Integer的构造方法
Integer integer=new Integer(10);Integer integer=new Integer("10");
自动装箱与自动拆箱
例如: Integer–>int 拆箱 int–>Integer 装箱
手动拆箱
这样是TRUE,因为包装类与基本数据类型运算的时候会自动拆箱,转换为基本数据类型
这样是FALSE,不应该使用==
,应该使用equals方法,new运算符一定会在堆内存当中创建
java中为了提高程序执行效率把-128到127所有的数字的包装类提前创建好,放到了一个方法区中的整数型常量池当中了,目的就是使用这个区间的数据就不需要再new了,直接从整数型常量池当中取
池:其实就是缓存(效率高,耗费内存),Integer类加载的时候会初始化256个整数型包装类对象
Integer里面有一个静态方法,传参String,返回int类型 :Integer.parseInt(“123”);如果传入非数字字符串就会产生以上异常
Interger.valueof();可以传入int和String类型转换为Integer类型,这个是静态方法
其他包装类也有类似的方法例如:Double.parseDouble(“3.14”);
String与int与Integer相互转换
对日期的处理
直接调用Date()的无参数构造方法,可以直接输出日期
还可以使用SimpleDateFormat进行日期格式化
有一个String日期字符串如何转换成Date()对象
如果SimpleDateFormat格式与字符串格式不一样转换就会出现异常(ParseException)
获取自从1970年1月1日 00:00:00:000到当前时间的总毫秒数
long currentTimeMillis = System.currentTimeMillis();
或者使用Date类型对象的getTime()方法
通过这个时间可以测试某一个方法运行完成所需要的时间
创建Date对象的时候还以直接在构造方法中传入毫秒数
Date date = new Date(System.currentTimeMillis());
数字格式化
这个会四舍五入,还有0表示不够补0;
BigDecimal 这个是引用数据类型,属于大数据,精度极高,多用于财务和金融
不能直接加减,要调用方法
随机数
随机数可以使用Random对象调用nextInt方法,会随机生成一个数,调用nextInt方法的时候还可以传输随机数的范围,传入100表示生成[0,100]之间的数
还可以使用Math类中的静态random方法生成出来的是double类型的数据
枚举
如果一个方法返回的结果有多种结果就需要使用到枚举了,那么把这些多种结果一个一个列举出,就是枚举,FALSE与TRUE实际上就是枚举,枚举使用enum定义。枚举编译之后也是生成class文件
枚举当中每一个枚举值都要规范大写,可以看做是常量
异常
平时打印的异常信息,是JVM打印出来的。java中的异常是以类和对象的形式存在的
例如:
java中的虚拟机发现程序当中有问题的话就会new出来一个对应的异常对象,并且抛出
出现异常程序就不会执行了,异常代码以下的就不会执行了,使用try盖住的话,异常代码以下的代码就会执行了
Error是不可处理的,一旦出现Error错误程序以下就不执行了,退出JVM,例如StackOverFlowError。Exception是异常是可以处理的,Error跟Exception 都是Throwable的子类,所以都是可抛出的
Exception的子类
Exception下面有两个子类,ExceptionSubClass是编译时异常,这个表示并不是指编译时候的异常,而是指在编写程序的时候必须对这种异常进行处理,如果不处理编译器就会报错 RuntimeException 是运行时异常(又称为未受检异常),用户可以选择处理或者不处理
编译时异常和运行时异常,都是发生在运行阶段。编译阶段是不会发生的。
编译时异常(又称为受检异常)因为什么而得名?
因为编译时异常必须在编译(编写)阶段预先处理,如果不处理编译器就会报错,所有的异常都是发生在运行阶段,因为只有程序运行的时候才会new对象
异常处理的两种方式
第一种:在方法声明的位置上,使用throws关键字(抛给上一级,但是上一级也要处理,可以抛出这个异常的父类(实际上使用了多态),也可以抛出多个异常),如果最终抛给了main方法,main方法不处理,继续往上抛,就抛给了JVM,这个时候java程序就不会执行了,终止执行
第二种:使用try…catch语句进行异常捕捉(自己处理)
这个时候hello world就不会执行了,因为第一句会出现异常,会new 出来一个异常对象,main方法没有处理,就抛给了JVM,程序就终止了,这个异常是ArithmeticException继承了RuntimeException,所以是运行时异常
这个需要处理,因为这个ClassNotFoundException不是运行时异常,自己写的类也需要处理
这个没要求强制处理,因为这个ArithmeticException是运行时异常
直接继承Exception的异常都是编译时异常,RuntimeException除外,如果想定义运行时异常需要继承RuntimeException,编译异常必须处理,运行时可以选择不处理,一般不在main方法上抛出异常
try语句块中一旦有一句代码出现了异常,异常代码下面一句就不会执行了,直接执行catch语句块,捕捉到了异常就不会上报了
catch语句块还可以有多个,但是捕捉的异常要遵循从小到大
在java8以后还可以这样写
什么时候上报?什么时候捕捉?
如果需要调用者处理就上报,其余使用捕捉。就这一条
Exception中还有两个方法getMessage()和printStackTrace()方法
打印异常信息的时候采用了异步线程方式,异常信息从上往下看,SUN公司写的代码就不用看了,看自己写的
finally子句的使用
在finally子句中的程序是一定会执行的,即使try语句块中的代码出现了异常
try可以直接与finally连用,可以不需要catch,finally语句块是一定会执行的,就算try中使用的return也会使用,唯一finally不执行代码是退出java虚拟机System.exit(0);
虽然fianlly中的代码一定会执行,但是返回的结果还是100
java中的代码必须遵循自上而下的顺序依次逐行执行(亘古不变)
java中return一旦执行,整个方法必须结束
反编译之后的代码:
final 与finally与finalize关键字的区别
final是一个关键字。表示最终的。不可变的。
finally要和try连用,是一定会执行的。
finalize是Object中的方法,表示JVM回收这个对象所执行的
自定义异常
两步骤
1.继承extends(编译异常)或者RuntimeException(运行时异常)
2.提供两个构造方法一个是无参数的,一个是带有String参数的
集合
集合是一个容器,一个载体,可以一次容纳多个对象
例如从数据库中一下查出十条数据,则就会把这些数据放到集合当中,然后传给前端
集合当中不能直接存储基本数据类型,另外集合也不能直接存储java对象,集合当中存储的都是对象的地址,list.add(100);这个会把100转换为包装类型,并把地址赋值给list集合
实际上集合也是一个对象,也有内存地址,集合里面也可以放集合
java中每一个不同的集合底层对应了不同的数据结构,往不同的集合中放数据,就相当于往不同的数据结构中存储数据 java中的所有的集合都在java.util包下面
java中的集合分为两大类,
一种是以单个方式存储元素 ,这一类集合中的超级父接口是java.util.Connection(继承了Iterable接口)
一种是以键值对的形式存储元素,这一类集合中的超级父接口是java.util.Map
Iterator是Iterable接口中的方法,返回一个迭代器对象,迭代器当中有hasNext()、next()、remove()方法
单个元素集合
List中的较重要三个实现类:ArrayList(数组,非线程安全的)、LinkedList(双向链表)、Vector(数组,线程安全的,但是效率不高,使用很少了,现在保证线程安全有其他的方案)
HashSet底层实际上使用的是HashMap集合,HashMap是一个哈希表数据结构
TreeSet底层实际上使用的是TreeMap集合,TreeMap集合底层采用了二叉树数据结构
SortedSet存储元素的特点,由于继承了Set集合也是无序不可重复,但是实现这个接口的集合,元素可以自动安从小到大排序
键值对集合
map集合以key和value这种键值对形式存储元素,key和value都是存储java对象地址,所有map集合的key都是无须不可重复的,这与Set集合相似,所以HashSet底层是用的就是HashMap,往HashSet添加元素实际上就是往HashMap的key里面放值
常见的实现有HashMap(非线程安全的) HashTable(线程安全的,效率不高,使用不多)
HashMap集合底层是哈希表,HashTable也是哈希表
SortedMap是继承Map的一个接口,SortedMap中的key首先是无序不可重复的,另外放在SortedMap中的key部分会按照从小到大进行排序,称为可排序的集合
Properties类型存储也是key和value形式的,但key和value只支持String类型,这是一个属性类
有序指的是放进去是有顺序的,取出来也是有顺序的
ArrayList:底层是数组
LinkedList:底层是双向链表
Vector:底层是数组,是线程安全的,效率低,较少使用
HashSet:底层是HashMap,放到HashSet集合中的元素等同于放到HashMap集合的key部分
TreeSet:底层是TreeMap,放到TreeSet集合中的元素等同于放到TreeMap集合key部分
HashMap:底层是哈希表
HashTable:底层也是哈希表,只不过是线程安全的,效率不高,较少使用
Properties:底层也是线程安全的,并且key和value只能存储String类型
TreeMap:底层是二叉树。TreeMap集合的key可以按照大小顺序排序
List集合存储元素的特点:是有序的可重复的
Set集合存储元素的特点:是无序不可重复的
SortedSet存储元素的特点:首先是无序不可重复的,但是SortedSet集合中的元素是可以排序的
Map集合的key,就是一个Set集合
往Set 集合种存储数据,实际上就是在Map集合中的key部分存储数据
Collection中的常用方法
add:添加元素 size:集合当中元素的个数 clear:清空集合 contains:判断集合当中是否包含某个元素
isEmpty:判断集合是否为空 remove:传入对象,删除集合当中的这个对象 removeAll:给集合A传入一个集合B,删除A集合中与集合B相交的元素 toArray:转换为一个对象数组
迭代器是所有Collection通用的一种方式,在Map集合当中不能使用,只能在Collection以及子类中使用
迭代器中有 hasNext、next、remove方法
迭代器最初没有指向第一个元素,next方法会让迭代器往下移一位,hasNext判断的是是否还有下一位元素,不管存进去的什么,取出来的都是Object对象,集合的大小是无限的,集合会自动扩容
迭代器是通用的
contains分析
contains底层使用的是equals方法比较集合当中时候包含传过来的元素
因为String重写了equals方法,所以为true
这个Stu没有重写equals方法,比较的是内存地址,所以为FALSE,如果重写了equals方法之后就是true了
remove底层调用的也是equals来比较传过来的对象与集合中的对象匹配,如果匹配到了就得到下标,删除这个元素
如果要删除集合当中有重复的元素,则只会删除第一个
集合只要发生改变,迭代器就要重新获取,否者使用以前迭代器的话就会出现ConcurrentModificationException异常
迭代器中的remove方法,删除迭代器当前指向集合中的元素,是原集合中的元素以及迭代器中的快照元素被删除了
迭代器其实就是对集合进行一次快照,迭代的时候会不断检查快照与原集合,如果使用集合的remove方法删除元素,迭代器就会发现快照与原来集合不一样了,就会报错,所以原集合发生改变,迭代器就一定要重新后去获取。而是用迭代器的remove方法删除会删除快照中的元素和原集合中的元素,这个时候快照与原集合就一样了,不会报错。
List接口中的特有方法
void add(int index,E element)方法:不会替换到原来的index位置的数据,使插入位置元素以下的都往下移动
Object set(int index,Object element):会替换到原来的index位置的数据,删除之前的元素,替换新的,其他位置元素不会改变
Object get(int index)方法:根据下标获取元素 int indexOf(Object o):返回元素第一次出现的下标位置
方法int lastIndexOf(Object o):返回元素最后一次出现的下标位置
Object remove(int index)方法:删除下标的元素,删除完成之后,后面的元素会往上移
ArrayList集合的初始容量和扩容
ArrayList 类是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制
private static final int DEFAULT_CAPACITY = 10;
ArrayList集合的初始容量是10,当添加第一个元素的时候才会初始化容量10,底层是一个Object类型的数组,在创建ArrayList集合的时候还可以指定初始化容量List list=new ArrayList(15);
当容量不够的时候,就会扩容,扩容大小是原来的容量的1.5倍
建议使用ArrayList的时候,预估使用容量,建议给定预估计的初始化容量,减少数组扩容次数
二进制的位运算
>>
加上位数n表示向右移n位,实际就是原数/(2的n次方)
<<
加上位数n表示向左移n位,实际就是原数*(2的n次方)
这么多的集合,你用哪一个集合最多?ArrayList集合,因为我们一般都是在末尾加数据,数组的检索效率高,随机增删比较低
构造一个ArrayList的时候还可以传入一个集合,将传入的集合转换为ArrayList集合
LinkedList分析
LinkedList也是有下标的,但不代表检索效率比较高,检索还是根据底层的数据结构,根据下标查询其实也是从头开始检索
LinkedList有一个first指针永远指向第一个,Last指针永远指向最后一个,LinkedList没有初始化容量这一说,添加一个元素就增加一个元素
Vector集合
Vector集合底层其实也是一个数组,与ArrayList区别就是Vector是线程安全的(中的方法都是synchronized修饰的),ArrayList是非线程安全的,
Vector的初始化容量也是10,Vector的扩容是原来的2倍,Vector集合构造的时候也可以指定初始化容量
List list=new ArrayList(); Collections.synchronizedList(list);
将非线程安全的ArrayList转换为线程安全的。
泛型
泛型是编译阶段一种特性,泛型只在编译阶段有效,运行的时候意义不大,使用了泛型1.集合当中存储的元素类型同一了,从集合中取出数据是指定的数据,不需要过多的强制转换,泛型会导致集合失去多样性,后面ArrayList的泛型JDK8之后就不用写了,自动类型推断,砖石表达式
自定义泛型
泛型常用E(element)和T(type) ,如果定义了泛型而不使用的话,结果是Object类型,所以List集合就可以加入多样的元素
接口也可以使用泛型
普通的泛型类
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
``public class Generic{
//key这个成员变量的类型为T,T的类型由外部指定
private T key;
public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
this.key = key;
}
public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
return key;
}
}
泛型接口
当实现泛型接口的类,未传入泛型实参时,实现类还是一个带泛型的类,泛型类型和接口的泛型一致,继承抽象类也是
当实现泛型接口的类,传入泛型实参时,实现类不是一个带泛型的类,泛型类型已经被确定了
Integer是继承Number的,假设有这样的第一方法,需要的参数是带有Number泛型的一个参数
public void m1(Gererator<Number> gererator){
}
Gererator<Integer> gererator=new GereratorImpl();
GereratorImpl.m1(gererator);
如果我们传入以上一个带有Integer泛型的参数就会报错
我们把代码改成这样就可以了,?表示参数集合的泛型,可以是任意类型
public void m1(Gererator<?> gererator){
System.out.println(gererator.test());
}
还有
//表示传过来的泛型要是继承E类的
public void m1(Gererator<? extends E> gererator){
System.out.println(gererator.test());
}
还有
//表示传过来的泛型要是要是T泛型或者T本身的父类
public void m1(Gererator<? super T> gererator){
System.out.println(gererator.test());
}
HashSet与TreeSet是不可重复的,底层判断是否重复使用的是equals方法
使用TreeSet的时候如果添加的是对象,就会报错
这是因为,TreeSet集合是会自动排序的,但添加的是对象的话,TreeSet不知道如何排序,就会报错,这个时候就需要对象类实现Comparable接口,实现compareTo方法,来自定义判断两个对象如何比较大小
Map集合
map跟collection没有任何的关系,map集合以key和value形式存在,value其实是key的附属品
常见方法
void clear():清空集合 boolean containsKey(Object key) :判断是否包含某个key
boolean containsValue(Object value):判断是否包含某个value V get(Object key):通过key获取value
boolean isEmpty() :判断集合是否为空 Set keySet():获取Map集合当中所有的key
V put(K key ,V value) :向集合当中添加一个键值对 V remove(Object key):通过key删除一个键值对
int size():Map集合的大小 Collection values():获取Map集合当中的所有value值
Set<Map.Entry<K,V>> entrySet():将map集合转换为Set集合,这个Set集合中的元素是Map.Entry<K,V>类型的这个是干啥的?看下图,Map.Entry<K,V>这个实际上是Map集合当中静态内部类
什么是静态内部类?
遍历Map集合的两种方式
第一种:
Set<String> strings = map.keySet();
for (String string : strings) {
System.out.print(string);
System.out.println(map.get(string));
}
}
第二种:
Set<Map.Entry<String, String>> entries = map.entrySet();
for (Map.Entry<String, String> entry : entries) {
System.out.print(entry.getKey());
System.out.println(entry.getValue());
}
哈希表
哈希表(散列表)其实就是数组里面存放链表
数组在查询方面效率很高,随机增删效率不高
单向链表随机增删效率较高,在查询方面效率不高
如果任意添加的节点元素的hash值都一样,那么这个哈希表就变成了单向链表
如果任意添加的节点元素的hash值都不一样,那么这个哈希表就变成了数组 这两种情况都成为称为散列分布不均匀
实现散列分布均匀,需要重写hashCode方法
数组中的每一个单向链表的hash值是相同的,同一个链表上任意两个节点的key的equals方法运算都是FALSE
哈希表就把这两个结合在一起
底层数组是Node<K,V>[] value
Node是HashMap的静态内部类,是HashMap的基本单位
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//哈希值是key通过hashCode()方法执行的结果,通过hash算法得到 //了,也可以看出数组的下标
final K key;//key
V value;//值
Node<K,V> next;执行下一个节点,因为这个是链表
}
put方法
添加元素的时候通过key经过hashCode方法得到hash值,通过哈希算法转换成数组下标,如果下标上没有任何元素,就把新的节点加到这个位置上。如果有元素的话,就遍历这个链表,并且遍历的时候比较每一个节点的key与新的节点的key进行比较(使用equals方法),如果都为false,则就加到这个链表的最后,如果有其中一个为TRUE,就会使用新的value值覆盖这个key对应的value值
get方法
通过传过来的key通过hashCode方法获得哈希值,通过哈希算法得到数组下标,通过数组下标定位到某一个位置,如果这个位置什么也没有,就返回null。如果有的话,就依次比较这个位置链表上的key与传过来的key比较是否相等(使用equlas)方法比较,如果有一个返回是TRUE,就返回这个节点的value,如果遍历这个链表到头都没有返回TRUE的,就返回null
哈希表的优点:
是无序(因为每次添加节点元素位置可能都不一样)不可重复(使用equlas来保证),增加和删除是在链表上完成,查询也只是部分查询
哈希表集合中的节点对象中的key部分,都会先后调用两个方法一个是equals方法,还有一个是hashCode方法
HashMap集合的默认初始化容量是16,默认加载因子是0.75,就是当HashMap集合的数组占用数量达到百分之75的时候,数组就开始扩容 HashMap集合可以在构造的时候传入默认容量,先预估计容量,在传入,但传入的数量建议是2的倍数!会提高HashMap集合的存取效率,为了达到散列均匀
如果一个类只重写了equals,没有重写hashCode方法,那么这个类的对象如果存储在哈希表中的话,那么可以存储两个equals为TRUE的两个对象,这是因为首先调用的是hashCode方法,生成的哈希值不一样,那么数组下标也不一样,这个时候就不会调用equals方法了。如果一个类的equals方法重写了,那么HashCode方法就一定要重写。
重写hashCode方法
@Override
public int hashCode() {
return Objects.hash(getName(), getAge());//可以根据业务要求自己写,例如希望年龄一样的在同一个链表,就不需要传入getName()了
}
如果每一个链表中的节点超过8个时并且数组长度到64,注意这个链表就会自动转换为红黑树,为了提高检索效率因为红黑树的查询效率较高(使用了二分法),如果这个红黑树中的节点个数少于6的时候,这个红黑树又会变为链表
HashMap集合的key可以为null,只能有一个。HashTable集合的key和value都不能为空,HashTable集合中的方法都有synchronized修饰的,是线程安全的,不过现在实现线程安全有其他方式,这个集合用的不多,HashTable集合的初始化容量为11,默认加载因子是0.75,扩展新的容量是原容量*2+1
Properties
Properties是一个Map集合,继承HashTable,key和value都必须为String,Properties是线程安全的
Properties properties=new Properties();
properties.setProperty("k1","v1");
properties.setProperty("k2","v2");
System.out.println(properties.getProperty("k1"));
TreeSet集合实际上是一个TreeMap集合,TreeMap集合是一个二叉树,放到TreeSet集合上的元素就是放到TreeMap集合上的key部分了,存储在TreeSet集合中的元素是无序不可重复的,会自动按照从大到小排序
TreeSet集合无法对自定义类型排序,如果再TreeSet集合当中添加没有实现Comparable接口的对象就会报错:
cannot be cast to java.lang.Comparable
TreeSet集合实现有序是把添加过来的元素强制转换成Comparable接口类型,所以添加的元素必须实现这个接口,String实现了Comparable接口,所有包装类也实现了,实现了compareTo方法,如果这个结果返回0就表示比较的对象与节点上的元素相等,返回大于0的表示传过来的元素比节点上的元素大,会继续在右子树上找,返回小于0的表示表示传过来的元素比节点上的元素小,会继续在左子树上找
假如一个类要以年龄大小比较,如果年龄大小一样,就比较姓名
@Override
public int compareTo(Teacher o) {
if(this.age==o.age){
//这里的this指向的是传过来的节点,o是指树上的节点
return this.name.compareTo(o.getName());
}
else{
return this.age-o.age;
}
}
二叉树
自平衡二叉树。遵循左小右大原则存放
遍历二叉树的三种方式 ,前序遍历(根左右),中序遍历(左根右),后序遍历(左右根)
TreeSet集合和TreeMap集合采用的是中序遍历方式,Iterator迭代器采用的就是中序遍历方式
中序遍历出来就是有序的,在添加元素的时候会一直调用传过来对象的compareTo方法,进行比较,添加元素的时候就排好序了
实现排序还可以创建一个比较器实现Comparator接口
class BiJiaoQi implements Comparator<Teacher> {
@Override
public int compare(Teacher o1, Teacher o2) {
return o1.getAge()-o2.getAge();
}
}
在创建TreeSet构造的时候传入这个比较器
Set<Teacher> set=new TreeSet(new BiJiaoQi());
这样就可以实现比较了
如果不想写这个类,还可以使用匿名内部类
Set<Teacher> set=new TreeSet(new Comparator<Teacher>() {
@Override
public int compare(Teacher o1, Teacher o2) {
return o1.getAge()-o2.getAge();
}
});
Collection集合工具类
sort方法:排序方法,对ArrayList进行排序的时候如果是对象的话,对象的类也要实现Comparable接口,既然要排序,就必须指定排序的规则
IO流
读Input是往内存里面加入数据,写output是往内存里面获取数据
流的分类
流的类都在java.io包下面,C++中的流需要自己写(doge)
一种是按照流的方向,以内存为参照物,从内存中出去,叫做输出,叫做写,进入内存里面,叫做输入,叫做读
一种是按照字节的方式读取数据(称为字节流),一次读取一个字节Byte,等同于8个二进制为,这种流是万能的,什么类型文件都可以读取,有的流是按照字符的方式读取数据的(称为字符流),一次读取一个字符,这种流是为了读取普通文件存在的,不能读取图片,视频,声音,连word都不能读取 例如a中国
采用字符流第一次读取‘a’,第二次读取‘中’,如果使用字节流的话,第二次就读到了’中’字的一半,第三次读到了’中’字的另一半,因为汉字占用两个字节
java中的IO流有大家族
如java.io.InputStream java.io.OutputStream java.io.Reader java.io.Writer
以Stream流结尾的都是字节流,其余都是字符流,这四个都是抽象类,这四个类都实现了Closeable接口,这个接口当中有一个关闭流的方法close方法,这表示全部的流都是可关闭的,使用流要养成使用完流就要及时关闭,要不然会浪费资源。OutputStream和Writer都实现了Flushable接口,Flushable接口有一个方法是flush方法,所有的输出流都是可刷新的,养成一个好习惯,用完输出流之后,一定要调用一下flush方法,将管道中剩余的数据全部输出,管道清空
java中需要掌握十六个流
文件专属
FileInputStream、FileOutputStream、FileReader、FileWriter、
转换流(将字节流转换成字符流)
InputStreamReader、OutputStreamWriter、
缓冲流专属
BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream、
数据流专属
DataInputStream、DataOutputStream、
对象专属流
ObjectInputStream、ObjectOutputStream、
标准输出流
PrintWriter、PrintStream
FileInputStream inputStream=null;
try {
inputStream=new FileInputStream("E:\\新建文件夹\\temp.txt");
System.out.println((char) inputStream.read());//读字节使得指针移动一位
System.out.println((char)inputStream.read());
}catch (FileNotFoundException e){//捕捉创建流对象抛出的异常
e.printStackTrace();
} catch (IOException e) {//捕捉read方法抛出的异常
e.printStackTrace();
}
finally {
if(inputStream!=null){//最后关闭流,先判断流是否为空
try {//关闭方法也会抛出编译异常
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
调用一次read方法其实就是移动流的指针指向下一位,如果到默认还继续读的话返回值就是-1,如果有中文就会出现只读取一半的情况,出现乱码
int value=0;
while ((value=inputStream.read())!=-1){
System.out.println((char)value);
}
输出全部数据
文件的路径还可以写相对路径,默认路径是当前工程的根目录
以上一次就读取一个字节效率不高,我们还可以传入一个byte的数组,byte数组的长度就是每次读的字节数量,inputStream.read(bytes)的返回值是读取字节的个数,下次读的时候还可以传入这个数组,但读的时候这个数组中的值不会被清空,而是被覆盖,例如文件中的数据是abcdef,每次读取四个,那么第一次读的数据就是abcd,第二次byte数组结果就是efcd
byte[] bytes=new byte[4];
System.out.println(inputStream.read(bytes));
最终版
public static void main(String[] args) {
FileInputStream inputStream=null;
try {
inputStream=new FileInputStream("E:\\新建文件夹\\temp.txt");
byte[] bytes=new byte[4];
int num=0;
while ((num=inputStream.read(bytes))!=-1){
System.out.print(new String(bytes,0,num));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(inputStream!=null){
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
int available方法:返回流中剩余没有读取的字节数量
long skip()方法:跳过几个字节不读·
甚至还可以这样写
byte[] bytes=new byte[inputStream.available()];
inputStream.read(bytes);
System.out.print(new String(bytes));
这种不能适用于大的文件,因为byte数组不能太大
FileOutputStream
public static void main(String[] args) {
FileOutputStream fileOutputStream=null;
try {
fileOutputStream=new FileOutputStream("myfile");
byte[] bytes=new byte[]{97,98,99};
fileOutputStream.write(bytes);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fileOutputStream!=null){
try {
fileOutputStream.flush();
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
,默认没写路径会在当前工程下面新建文件,使用write方法也可以往文件当中写数据,也可以指定从数组的哪里开始到哪里结束,这种写是覆盖之前文件中的内容重新写入,会将源文件清空在写入,如果想追加的话可以在构造流的时候传入参数
fileOutputStream=new FileOutputStream("myfile1",true);//传入TRUE表示是追加
文件的复制
public static void main(String[] args) {
FileInputStream inputStream=null;
FileOutputStream outputStream=null;
try {
inputStream=new FileInputStream("E:\\新建文件夹\\6 - What If I Want to Move Faster.mp4");
outputStream=new FileOutputStream("E:\\新建文件夹 (2)\\copy.mp4");
byte[] bytes=new byte[1024*1024];
int num=0;
while ((num=inputStream.read(bytes))!=-1){
outputStream.write(bytes,0,num);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(inputStream!=null){
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(outputStream!=null){
try {
outputStream.flush();outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
FileReader与FileWriter
这两个都是字符流,这个读取中文不会乱码,这个读的时候需要传入字符数组
public static void main(String[] args) {
FileReader fileReader=null;
try {
fileReader =new FileReader("myfile1");
char[] chars=new char[2];
int num=0;
while((num=fileReader.read(chars))!=-1){
System.out.println(new String(chars,0,num));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fileReader!=null){
try {
fileReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
FileWriter跟FileOutputStream类似
FileWriter writer=null;
try {
writer=new FileWriter("myfile1",true);
writer.write("我是中国人");
} catch (IOException e) {
e.printStackTrace();
}
可以传入字符数组还可以直接传入字符串
这两种流只能读取普通文本(能用记事本编辑的都是普通文本文件)
缓存流
当一个流的方法中需要一个流的话,这个传过来的流叫做节点流,外部负责包装的流叫做包装流
例如:
BufferedReader bufferedReader= new BufferedReader(new FileReader(“myfile1”));
关闭的时候只需要关闭BufferedReader 流就可以自动关闭FileReader流了,关闭底层实际上关闭的就是FileReader流。对于包装流来说,只要关闭包装流,里面的节点流就会自动关闭
方法:readLine()方法可以读取一行,返回的是String类型,如果没有就返回null
读取全部的,这个方法是没有读取换行符的
String line=null;
while ((line=bufferedReader.readLine())!=null){
System.out.println(line);
}
BufferedReader在传入的时候只能传入字符流,不能传入字节流,如果想传入字节流就需要使用到转换流
FileInputStream fileInputStream = new FileInputStream("myfile1");
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(fileInputStream));
数据流
这个流可以连通数据连通数据的类型一并写入到文件
这个文件不是普通文本文件,这种写的文件是看不懂的
DataOutputStream dataOutputStream=new DataOutputStream(new FileOutputStream("myfile12"));
byte b=100;
short s=120;
int i=300;
String s1="123";
boolean bo=false;
dataOutputStream.writeByte(b);
dataOutputStream.writeShort(s);
dataOutputStream.writeInt(i);
dataOutputStream.writeChars(s1);
dataOutputStream.writeBoolean(bo);
dataOutputStream.flush();
dataOutputStream.close();
写的文件都是乱码,这种写进去的数据只有使用DataInuputStream才能获取到,并且读的数据类型的顺序要跟写入的时候一样,例如第一个就要readByte第二个就要读取readShort
标准输出流
System类的out属性就是一个PrintStream对象,println是PrintStream对象的一个方法
PrintStream printStream=System.out;
printStream.println("hello world!");
甚至我们还可以设置打印方法的输出方向
System.setOut(new PrintStream(new FileOutputStream("logFile.txt")));
System.out.println("123");
这样就不会输出在控制台了,会输出在项目目录的logFile.text中,这其实就是一些日志框架的实现原理!!!
如果创建了一个工具类静态的改变了打印方法的输出方向,那么调用这个静态方法的原方法的打印方法也会改变输出方向,如果再静态方法当中在创建一个新的PrintStream对象就不会了
class Logger{
public static void log(String s){
PrintStream printStream=null;
try {
printStream =new PrintStream(new FileOutputStream("logFile.txt",true));
printStream.println(s);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
File类
任何文件和目录都可以看成一个File对象,是文件和或者一个目录的抽象表示。File跟流没有什么关系,所以File类不能完成文件的读和写,一个File对象可能是目录也可能是一个文件
exists:判断文件和目录是否存在 createNewFile():如果不存在就新建一个文件 mkdir():如果不存在就新建一个目录 mkdirs():如果目录当中多个目录都不存在,则会新建多个目录
getAbsolutePath():获取绝对路径 getParent获取父路径 isDirectory():判断是否一个目录 isFile():判断是否一个文件 lastModified()获取最后一次修改的时间,是从1970年到最后修改时间段的毫秒数
listFiles():获取一个目录下所有的文件,返回值是一个File类型的数组
序列化与反序列化
把java对象放到硬盘上面的过程就叫做序列化,序列化对象是把对象拆分成一块一块的,传输到硬盘上,把每一小块编号,这个过程就叫做序列化(Serialize),使用到ObjectOutputStream
将硬盘上的文件转换成java对象的过程叫做反序列化,把硬盘上的对象每一小块的根据编号组装称为一个对象放到内存里面,这个过程就叫做反序列化(DeSerialize),使用的ObjectInputStream
参与序列化和反序列化的对象必须实现Serializable接口,这个接口只是一个标志性接口,里面什么也没有,这叫做标志性接口,标志性接口里面都没有任何东西,JVM看到这个类实现Serializable接口,会自动生成一个序列化版本号。
实现序列化:
Student student=new Student("郭建权",20);
ObjectOutputStream objectOutputStream=new ObjectOutputStream(new FileOutputStream("mystu"));
objectOutputStream.writeObject(student);//还可以传入一个List集合(也实现了Serializable接口),一次性序列化多个对象
反序列化:
Student student=null;
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream("mystu"));
Object readObject = objectInputStream.readObject();
System.out.println((Student)readObject);
transient关键字
这个修饰的属性表示不会被序列化,例如:
private transient String name;
序列化版本号
如果一个类在序列化之后,进行了修改,这个时候如果没有手动写序列化版本号,那么新修改的类的由JVM编译生成的序列号与JVM编译旧的类生成的序列号不一样,那么这个时候反序列化就会报错java.io.InvalidClassException,序列化版本号不一致错误
java当中是如何区分类名的?
首先比较类名,如果类名不一样就会比较序列号版本号是否一样
这种自动生成的序列化版本号的缺点是一旦代码生成,以后不能修改了,一修改JVM就会重新编译生成新的序列化版本号,建议提供一个固定不变的序列化版本号,这样以后即使这个类代码修改了,JVM还会认为这是一个类
重新编写序列号就在类中添加一个常量
private static final long serialVersionUID = 1L;
String 就是了重新编写序列号
IO与properties的联合使用
我们想把一个文件中是键值对的数据放到Properties里面
我们新建一个properties后缀名结尾的文件,在java中称为属性配置文件,属性配置文件中的注释是在数据前面加#
,数据中尽量不要有空格,键与值之间不一定要使用=
还可以使用:
public static void main(String[] args) throws Exception {
File file=new File("pro.properties");
FileReader reader=new FileReader(file);
Properties properties=new Properties();
properties.load(reader);
System.out.println(properties.getProperty("admin"));
System.out.println(properties.getProperty("password"));
}
我们使用以上代码就可以将属性配置文件中的键值对存储到Properties集合当中,如果key重复了则value值自动覆盖
多线程
进程与线程区别
进程是一个应用程序,线程是一个进程的中的执行单元/或者说执行场景,一个进程可以启动多个线程
例如我们启动一个main方法的时候,就会启动一个线程运行main方法,再启动一个线程负责垃圾回收,java程序一启动就至少有两个线程
进程与进程之间内存独立不共享,开辟一个线程就会多出一个栈内存,栈内存之间是独立的,堆内存与方法区是只有一个的 ,这些线程栈是共享堆内存与方法区,假如启动10个线程就会有10个栈空间,各自互不干扰
使用多线程是为了提高效率,使用多线程之后main方法结束后,程序也不一定结束,只是主线程结束了
多核心cpu在同一时间上是可以是实现真正的多线程并发的,但对于单个核心的cpu来说,是可以实现出一种多线程的错觉,单个核心在同一时间只能执行一个线程,cpu运算很快,线程之间来回切换。就跟看视频一样。
java中实现线程主要有两种方式
1.编写一个类,直接继承java.lang.Thread,重写run方法,
public class ThreadDemo01 {
public static void main(String[] args) {
MyThread myThread=new MyThread();
myThread.start();
for (int i = 0; i < 100; i++) {
System.out.println("主线程"+i);
}
System.out.println("主线程结束了");
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程"+i);
}
}
}
start()方法就是启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后就瞬间结束了。新的线程启动成功之后就会自动调用run方法,run方法在分支栈的底部,main方法在主线程栈的底部。如果直接调用run方法的话就不会创建新的线程,就相当于调用普通方法,还会在主线程当中运行,run方法必须重写。新的栈与主栈之间互不干扰,各自执行各自的,并发的
还可以实现Runnable接口,在创建Thread对象的时候传入这个接口的实现对象
public class ThreadDemo01 {
public static void main(String[] args) {
Thread thread=new Thread(new MyThread2());
thread.start();
for (int i = 0; i < 100; i++) {
System.out.println("主线程"+i);
}
System.out.println("主线程结束了");
}
}
class MyThread2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程2"+i);
}
}
}
还可以使用匿名内部类方式,使用最多的就是实现接口,如果使用继承的话就不能继承其他类了
线程的生命周期
刚new出来的对象为新建状态–>启动start方法进入就绪状态<–>run方法的开始执行表示进入运行状态–>run方法执行完成进入死亡状态
就绪状态表示当前线程具有抢夺cpu时间片的权利(就是执行权)。当一个线程抢夺到时间片就会执行run方法进入运行状态,在运行状态的时候也就是run方法执行的时候,如果时间片使用完了之后就会由JVM调度到就绪状态继续抢夺CPU时间片,抢到之后继续回到run方式上一次执行的代码继续执行
当一个线程遇到阻塞事件,例如接收用户输入或者sleep方法,就会进入阻塞状态,阻塞状态的线程会放弃之前占有的CPU时间片
,进入就绪状态继续抢夺CPU时间片
线程对象可以设置名字跟获取名字,不设置默认是Thread-N从0开始递增
获取当前线程对象
Thread thread = Thread.currentThread();
在段代码在哪里获取的就是当前线程的名字,在线程类当中有点像this。
sleep方法,也是一个静态方法,谁执行我我就让谁休眠,单位是毫秒,既然我被执行肯定会有一个线程来执行我,跟Thread.currentThread()方法一样
Thread.sleep(10000);
这个可以每隔一段时间干一次事情,例如
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
System.out.println(i);
Thread.sleep(1000);
}
}
一个面试题
run方法当中的异常只能try…catch处理
interrupt()方法是终止线程的休眠,使用的异常机制,让休眠的那一块代码出现InterruptedException: sleep interrupted异常,这样就会继续往下面执行了
public class demo05 {
public static void main(String[] args) {
MMThread mmThread=new MMThread();
mmThread.start();
mmThread.interrupt();
}
}
class MMThread extends Thread{
@Override
public void run() {
System.out.println("begin");
try {
Thread.sleep(1000*60);
System.out.println(123);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end");
}
}
stop方法可以强制执行一个正在执行的线程,这个会丢失数据,方法已过时了
如何合理终止一个线程?
一般打一个布尔标志
public class demo05 {
public static void main(String[] args) {
MMThread mmThread=new MMThread();
mmThread.run=true;
mmThread.start();
mmThread.run=false;
}
}
class MMThread extends Thread{
boolean run;
@Override
public void run() {
if(run){
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}else {
//程序结束可以在程序结束之前保存一些数据,可以在这里写
System.out.println(123);//模拟线程销毁前保存的数据
return;
}
}
}
常见的线程调度模型
1.强占式的调度模型:抢到cup时间片的概率就高一些,java采用的就是这个
2.均分式调度模型:一切平等
线程调度的方法:
实例方法:void setPriority和getPriority,最低的是1,默认是5,最高是10
让位方法 :yield:暂停当前正在执行的线程,执行其他线程,不是阻塞,是一个静态方法
让线程从运行状态回到就绪状态
join:合并线程方法,是一个实例方法,一个线程对象t执行这个方法后,当前线程就会进入阻塞状态,线程t全部执行完成之后,才会开始执行当前线程
线程安全
线程安全是重点
什么时候数据再多线程情况下会出现线程安全问题1.多线程环境,2.有共享数据 3.共享数据有修改行为
假如我们一个对象进行取款操作,那么两个线程去取款,共享的是一个对象,那么就会出现线程安全问题
如何解决线程安全问题?那就是线程排队执行专业术语叫做线程同步,会牺牲一些效率
异步编程模型:线程t1与线程t2各自执行各自的,互不干扰,其实就是多线程
同步编程模型:线程t1与线程2执行的时候,在某一时刻只能执行一个线程,线程之间发生了等待关系
解决线程安全问题
同步代码块
synchronized(){}
小括号里面怎么写?那要看你想要哪些线程同步。那就在小括号里面写这些线程中的共享对象
在java语言当中每一个对象其实都会有一把锁,其实这把锁就是标记,100个对象100个锁,假设t1先执行了遇到synchronized,就会自动找后面括号里面共享对象的对象锁,找到之后并占有这把锁,然后执行同步代码块中的内容,在程序执行过程中一直都是占有这把锁的。直到同步代码块执行完成,这把锁才会释放。
假设t1正在占有某个对象的对象锁,此时t2也遇到了synchronized关键字,也会去后面占有对象的对象锁,结果这把锁被t1占用了,t2只能在同步代码块外面等待着t1的结束,直到t1把同步代码块执行完毕了,释放这把锁,t2才会占有这把锁,执行同步代码块,这样就达到了线程排队执行,这个共享对象一定要选好,这个共享对象一定要是你想同步的线程所共享的对象
当一个线程遇到了synchronized关键字,会进入锁池(lockpool)中去寻找共享对象的对象锁,会释放之前占有的时间片,如果没找到就在锁池中等待,如果找到了就进入就绪状态继续抢夺CPU时间片
甚至写一个字符串也行,但是传入一个字符串会让所有的线程同步
局部变量永远都不会存在线程安全问题,因为局部变量在栈中是不共享的,实例变量和静态变量都会有线程安全问题,因为堆内存和方法区内存只有一个
同步代码块越小效率越高,在实例方法上可以直接加synchronized关键字,那锁中的默认值就是this,这种方式不灵活,同步的是整个方法体,导致的程序效率降低。StringBuffer使用的就是这个方式,如果使用局部变量的话就可以使用StringBuilder,在集合当中如果使用在局部变量使用线程不安全的
synchronized有三种写法,一种是synchronized(){同步代码块}
,这种灵活,还有一种是在方法上写,这种共享对象是this,同步代码块是整个方法体,第三种是在静态方法上使用synchronized,表示的是整个类是锁,100个对象的类锁是一个!类锁是为了保证静态变量的安全!
synchronized面试题1
如果一个类中有m1与m2方法,m1方法添加了synchronized关键字,m2没有,那么如果另外一个线程调用m2的话不需要等m1释放对象的锁就可以执行。如果m2有synchronized关键字就需要m1释放锁了。如果该类中有一个m3的静态方法使用的synchronized关键字,那么无论创建了多少对象,都需要等待!!!
死锁
假如一个线程A先锁住了A对象,同一时间B线程锁住了B对象,接下来A线程又要去锁B对象结果发现B对象被锁了,就会继续等待,同一时间B线程去锁A对象结果发现A对象被锁了,那么这两个线程就会继续等待对方先放锁,这样就会出现死锁
死锁代码
package com.gjq.xc;
public class siSuoDemo {
public static void main(String[] args) {
Object o1=new Object();//两个线程共享的对象o1
Object o2=new Object();//两个线程共享的对象o2
MyThread1 myThread1=new MyThread1(o1,o2);
MyThread2 myThread2=new MyThread2(o1,o2);
myThread1.start();
myThread2.start();
}
}
class MyThread1 extends Thread{
Object o1;
Object o2;
public MyThread1(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o1){//先锁o1,然后休眠1s,再锁o2,如果o2没有锁成功,那么o1就不会释放
try {
Thread.sleep(1000);//休眠表示让两个线程都先锁住自己先锁的那一个对象
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
}
}
}
}
class MyThread2 extends Thread{
Object o1;
Object o2;
public MyThread2(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o2){//先锁o2,然后休眠1s,再锁o1,如果o1没有锁成功,那么o2就不会释放
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
}
}
}
}
我们在开发当中到底如何解决线程安全问题?
不是一上来就是用ynchronized关键字,这样效率不高,在不得已的情况下在使用。
在开发中尽量使用局部变量代替实例变量和静态变量,如果是实例变量,那么可以考虑创建多个对象,这样一个线程对于一个对象,对象不共享就没有数据安全问题了
守护线程
守护线程其实就是后台线程,例如gc机制就是一个守护线程,java语言当中线程分为用户线程和守护线程
守护线程一般是一个死循环,一直在做某一件事,所有的用户线程一旦结束,守护线程就会结束。实现功能:例如每天零点的时候系统数据自动备份
实例方法setDaemon可将普通线程设置成守护线程
定时器
定时器的作用:间隔特定的时间执行特定的程序
java中可以使用多种方式实现定时执行任务1.使用线程,可以使用sleep方法,这是原始的计时器
2.java中类库中已经写好了定时器:java.util.Timer,可以直接拿来用,不过这种方式在开发中也不多用,因为有框架(doge),例如SpringTask框架,这个框架院原理其实就是这个java的计时器原理
public class timer01 {
public static void main(String[] args) throws Exception{
Timer timer=new Timer();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date parse = simpleDateFormat.parse("2022-02-15 13:06:30");
timer.schedule(new Sch(),parse,1000);
}
}
class Sch extends TimerTask {
@Override
public void run() {
System.out.println("数据备份");
}
}
实现线程的第三种方式
之前的那两种实现方式是无法获取线程返回值的,run方法返回值都是void,如果想得到线程中返回的结果,就需要使用第三种方式,实现Callable接口
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable callable;
//使用匿名内部类方法,new接口方式
FutureTask futureTask=new FutureTask<>(new Callable<Object>() {
@Override
//重写call方法,就跟重写run方法一样,这个能返回值
public Object call() throws Exception {
Thread.sleep(1000);//模拟休眠1s,主线程获得我这个线程的时候就需要等待我,主线程就会进入阻塞状态
return 1+2;
}
});
Thread thread=new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());//使用FutureTask对象的get方法获得线程值,这个时候主线程会进入阻塞状态,等待call方法返回值
}
wait和notify
这两个方法是Object类中自带的
Object o=new Object();o.wait()
这是让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止,wait方法的调用会让当前线程进入等待状态,o.notify方法的调用,可以让正在o对象上等待的线程唤醒,notifyAll方法可以唤醒所有等待的线程
这俩方法有什么用?
消费者模式和生产者模式
wait方法会让正在o对象上的活动的线程进入等待状态,并且释放o对象的锁,notify方法只会通知,不会释放之前占有的锁。wait方法和notify方法建立在线程同步的基础之上。
package com.gjq;
import java.util.ArrayList;
import java.util.List;
public class ProsucerAndConsummer {
public static void main(String[] args) {
List list=new ArrayList();
Thread t1=new Thread(new Consumer(list));
Thread t2=new Thread(new Producer(list));
t1.setName("Consumer");
t2.setName("Producer");
t1.start();
t2.start();
}
}
class Consumer implements Runnable{
//仓库
List list;
public Consumer(List list) {
this.list = list;
}
@Override
public void run() {
synchronized (list){//运行的时候锁住仓库
while (true){
if(list.size()==0){
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//消费者的wait方法过来的话就一定会执行以下代码,上面if判断不会执行
Object remove = list.remove(0);
System.out.println("消费者消费了"+remove);
list.notifyAll();//如果是Producer的wait方法使得这个Consumer线程进行消费,消费完成之后,会唤醒之前的Producer线程进行生成
}
}
}
}
class Producer implements Runnable{
//仓库
List list;
public Producer(List list) {
this.list = list;
}
@Override
public void run() {
synchronized (list){//运行的时候锁住仓库
while (true){
if(list.size()>0){
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//生产者的wait方法过来的话就一定会执行以下代码,上面if判断不会执行
Object o =new Object();
list.add(o);
System.out.println("生产者生产了"+o);
list.notifyAll();//如果是第一次生产,唤醒两个全部线程之后,如果Producer又抢到了线程就会锁住list并且进去if语句
//使得Producer线程释放锁并且等待,这个时候Consumer线程就会抢到时间片进行消费。
}
}
}
}
反射机制
通过java中的反射机制可以操作字节码文件
可以读取和改动字节码文件,java反射机制都在java.lang.reflect包下面,主要学习这个包下面的Method(代表字节码中的方法字节码)与Constructor(代表字节码中的构造方法字节码)与Field(代表字节码文件当中的属性字节码)与Class(代表字节码文件)
获取Class的三种方式
Class c1=Class.forName("java.lang.String");
c1代表的就是String类的字节码文件
第二种方式:java中的任何一个对象都有一个getClass方法,这是Object中的,返回的是这个对象对应类的Class对象,多个同一类的对象获得的Class对象内存地址是一样的,Class对象是同一个,这个Class对象在方法区当中,Class对象指向的是在方法区中的Class文件
Class c1=Class.forName("java.lang.String");
Class c2 = "123".getClass();
System.out.println(c1==c2);结果是true
第三种是使用类名获取属性class
Class c3 = String.class;
可以使用Class对象的newInstance();创建一个对象,使用的是类的无参构造方法,如果没有了无参构造方法,这个时候就会报错了InstantiationException实例化异常
这个创建对象更加灵活,哪里灵活?
我们创建一个properties配置文件,里面写
className=com.gjq.reflect.User
我们可以获取这个配置文件中的className的值并且通过反射实例化这个对象
public static void main(String[] args) throws Exception{
FileReader fileReader=new FileReader("Class.properties");
Properties properties=new Properties();
properties.load(fileReader);
Class c=Class.forName(properties.getProperty("className"));
Object o = c.newInstance();
System.out.println(o);
}
这样如果我们想要修改创建的对象的话就直接修改配置文件就好了,这样符合OCP原则,反射使得程序更加灵活,许多框架使用的就是这个机制
如果想一个类的静态代码块执行去,其他代码不执行可以使用Class.forName(“com.gjq.reflect.User”);,这句代码会导致类加载,类加载就会调用静态代码块
文件路径问题
如果再idea工具当中写的相对路径的代码,如果把这些代码移植到其他上面,可能就会出错了
就要使用一种通用的方式,这个方式前提是文件必须在类路径下面,src下的就是类路径,src是类的根路径
Thread.currentThread().getContextClassLoader().getResource("Class.properties").getPath()
就可以获得项目类路径上的资源在硬盘上的地址,默认是在src下面寻找,所以Class.properties文件要放在src目录下面,这种方式在Linux环境下也可以使用
我们还可以直接以流的形式返回
Thread.currentThread().getContextClassLoader().getResourceAsStream("com/gjq/Class.properties");
我们还可以使用资源绑定器实现
ResourceBundle bundle=ResourceBundle.getBundle("com/gjq/file/Class");
bundle.getString("className");
这种不用写后缀名,默认是properties文件文件
类加载器
专门负责加载类的命令/工具
启动类加载器(父加载器) 扩展类加载器(母加载器) 应用类加载器
程序开始执行之前,会将所需的类全部加载到JVM当中,首先通过启动类加载器专门加载jre环境下面的lib\rt.jar中的类,如果启动类加载器加载不到的就会使用扩展类加载器专门加载lib/目录下的ext目录中的jar包,如果扩展类加载器也没有加载器,就会使用应用类加载器专门加载classpath中的jar包或者classpath文件
java为了保证类加载的安全使用了双亲委派机制:优先从父加载器中加载,然后咋从母加载器中加载,然后再应用类加载器加载,这也是为了安全,如果黑客写了一个java.lang.String类,如果不按照这种加载机制,那么可能会加载黑客编写的这个String类,就会替换java中原本的String,这样就会被破坏
Class对象的getDeclaredFields():方法可以获得对象的全部属性对象(Field),不管其修饰符 getFields()是获得所有以public修饰的属性,getName()方法获得类名,getSimpleName()获得不带包名的类名
Field对象使用getType()获得属性的类型封装成一个Class对象(Field属性对象的类型封装而成的Class对象)
getModifiers()方法用于获得属性前面的修饰符,返回值是一个int类型的,是多个修饰符的组合起来通过某些算法转换成为一个数字,我们可以使用Modifier.toString转换成为一个字符串
int modifiers = field.getModifiers();
System.out.println(Modifier.toString(modifiers));
反编译
Class<?> c1 = Class.forName(bundle.getString("className"));
StringBuilder builder=new StringBuilder();
builder.append(Modifier.toString(c1.getModifiers())+" class "+c1.getSimpleName()+"{");
builder.append("\n");
Field[] fields = c1.getDeclaredFields();
for (Field field : fields) {
builder.append("\t");
builder.append(Modifier.toString(field.getModifiers()));
builder.append(" ");
builder.append(field.getType().getSimpleName());
builder.append(" ");
builder.append(field.getName());
builder.append("\n");
}
builder.append("}");
System.out.println(builder);
以上代码通过一个类名就可以获得这个类的所有属性!!
访问对象的属性,Spring框架底层就是用了这个原理
Class c1=Class.forName("com.gjq.reflect.User");//获取反射Class对象
Object newInstance = c1.newInstance();//通过反射对象实例化一个对象
Field nameField=c1.getDeclaredField("name");//获取反射对象的name属性,私有的属性不能直接这样访问
//nameField.setAccessible(true);为了private修饰的属性也能访问到,打破属性的封装(不安全)
nameField.set(newInstance,"曹孟德");//通过属性对象的set方法给新创建的对象设置该属性的值
System.out.println(nameField.get(newInstance));//通过属性对象的get方法获取某个对象的该属性值
可变成参数在方法参数写入int…args,表示可以传入0到任意个int类型参数,如果还有其他参数,可变成参数必须写到参数列表中的最后一个,可变长参数在参数列表中只能有一个,可变长参数可以当成一个数组
Method
getDeclaredMethods()可以获得所用方法对象封装成为Method对象数组
Method对象的getModifiers()方法获得方法的修饰符,getReturnType()获得方法的返回值类型,getName()获得方法名,getParameterTypes();方法获得方法的所有参数类型封装成一个Class数组
通过反射机制调用方法
假设有一个UserService类中有一个login方法
jpublic class UserService {
public boolean login(String name,String password){
return (name.equals("admin"))&&(password.equals("123"));
}
public void logout(){
System.out.println("退出登录");
}
}
调用方法代码
public class UserServiceReflect {
public static void main(String[] args) throws Exception{
Class<?> aClass = Class.forName("com.gjq.reflect.UserService");
//通过反射对象获得指定方法,指定方法需要指定方法名和参数列表,这里参数列表传入参数的反射对象
Method login = aClass.getDeclaredMethod("login", String.class, String.class);
//创建一个反射对象
Object o=aClass.newInstance();
//调用获得的反射方法,第一个参数调用哪一个对象的方法,后面是方法的参数
System.out.println(login.invoke(o, "admin", "123"));
}
}
调用构造方法
区分构造方法只需要传入参数列表区分
有一个Student类
public class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
通过反射调用这个类的全参构造方法
public static void main(String[] args) throws Exception{
Class<?> aClass = Class.forName("com.gjq.reflect.Student");
Constructor constructor= aClass.getDeclaredConstructor(String.class,int.class);
Object guojianquan = constructor.newInstance("guojianquan", 19);
System.out.println(guojianquan);
}
通过反射获得类的父类和这个类的实现接口
我们获得String类的父类和这个类的实现接口
public static void main(String[] args)throws Exception {
Class<?> aClass = Class.forName("java.lang.String");
Class<?> superclass = aClass.getSuperclass();
Class<?>[] interfaces = aClass.getInterfaces();
}
注解
注解(Annotation),是一种引用数据类型,也是生成class文件
自定义注解:[修饰符列表] @interface 注解类型名{}
注解可以出现在类、属性、方法、变量、接口、枚举、注解等,默认可以出现在任何上面
java.lang包中的内置的注解:Deprecated(掌握)、Override(掌握)、SuppressWarnings(不需要掌握)
Override注解:这个注解只能注解方法,这个注解是给编译器参考的,运行阶段没有关系,只要方法当中有这个注解的话,编译器就会自动编译检查,如果不是重写父类的方法,编译器就会报错
如果一个注解A修饰别的注解B,那么这个A注解叫做元注解
常见的元注解有Target,Retention注解
Target注解用来标注被标注的注解能出现在哪些位置上面,例如@Target(ElementType.METHOD)表示只能出现在方法上面,ElementType是一个枚举
Retention注解用来标注的注解保存在哪里,例如@Retention(RetentionPolicy.SOURCE)表示被标注的注解保存到java源文件当中,RetentionPolicy其实是一个枚举,里面还有RetentionPolicy.CLASS这个表示保存到class文件和RetentionPolicy.RUNTIME这个表示保存在class文件中并且可以被反射机制获取
Deprecated注解:表示被注解的东西已经过时,调用这个东西的时候会出现横线,JDK1.8这个注解里面什么也没有
注解当中定义属性
[修饰符] 属性类型 属性名(),修饰符不能使用private修饰,注解当中如果有属性,使用注解的时候就必须给属性赋值,多个属性使用逗号分开
例如,有一个注解
public @interface GJQ {
String name();
int age() default 20;//指定一个默认值,这个使用的时候不传值也行
}
使用的时候
@GJQ(name = "gjq")
public class AnnDemo {
}
当注解当中只有一个属性,并且属性名字为value时,在使用的时候可以省略属性名去赋值
public @interface GJQ {
String value();
}
使用的时候
@GJQ("guojianquan")
public class AnnDemo {
}
注解中的属性可以使基本数据类型和String、Class、枚举,以及上面每一种的数组形式
如果一个注解的属性是一个数组,如果是value可以省略属性名
public @interface GJQ {
String[] hobbies();
}
@GJQ(hobbies={"basketball","pingpang"})//使用的时候使用大括号存放,每一个使用逗号分开
public class AnnDemo {
}
如果属性数组只有一个值的话就可以省略,可以这样写
@GJQ(hobbies="basketball")
反射注解
@GJQ(hobbies="basketball")
public class AnnDemo {
public static void main(String[] args) throws Exception{
Class aClass = Class.forName("com.gjq.Annotation.AnnDemo");
System.out.println(aClass.isAnnotationPresent(GJQ.class));返回值true
}
}
注解上必须加Retention注解修饰为RetentionPolicy.RUNTIME,要不然反射机制拿不到
@Retention(RetentionPolicy.RUNTIME)
public @interface GJQ {
String[] hobbies();
}
反射获取注解的属性值
@GJQ(hobbies={"basketball","football"})
public class AnnDemo {
public static void main(String[] args) throws Exception{
Class aClass = Class.forName("com.gjq.Annotation.AnnDemo");
if(aClass.isAnnotationPresent(GJQ.class)){//判断是否被GJQ注解修饰
GJQ GJQannotation =(GJQ) aClass.getAnnotation(GJQ.class);如果被GJQ修饰的话就获取这个注解对象强转成GJQ注解
String[] ss=GJQannotation.hobbies();//获取类上注解的值
for (String s : ss) {//遍历属性值
System.out.println(s);
}
}
}
}
注解在开发当中有什么用??
假设有一个@ID注解,只能修饰在类上面,表示被这个注解修饰的必须有一个int类型的id属性
代码如下
package com.gjq;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
public class finalDmeo {
public static void main(String[] args)throws Exception {
Class<?> aClass = Class.forName("com.gjq.User");
if(aClass.isAnnotationPresent(ID.class)){
boolean isOK=false;//判断是否包含int的id属性的标志
Field[] fields = aClass.getDeclaredFields();//反射获取类中的所有属性
for (Field field : fields) {//遍历属性
if((field.getName().equals("id"))&&(field.getType().getSimpleName().equals("int"))){//如果有属性名是id,并且为int类型,就isOK赋值为TRUE
isOK=true;
break;
}
}
if(!isOK){如果为TRUE,说明没有int类型的id属性
throw new HasNotIdException("没有ID属性异常");
}
}
}
}
@Retention(RetentionPolicy.RUNTIME)
@interface ID{
}
@ID
class User{
private int idd;
private String name;
}
class HasNotIdException extends RuntimeException{
public HasNotIdException() {
}
public HasNotIdException(String message) {
super(message);
}
}
s={"basketball","pingpang"})//使用的时候使用大括号存放,每一个使用逗号分开
public class AnnDemo {
}
如果属性数组只有一个值的话就可以省略,可以这样写
@GJQ(hobbies="basketball")
反射注解
@GJQ(hobbies="basketball")
public class AnnDemo {
public static void main(String[] args) throws Exception{
Class aClass = Class.forName("com.gjq.Annotation.AnnDemo");
System.out.println(aClass.isAnnotationPresent(GJQ.class));返回值true
}
}
注解上必须加Retention注解修饰为RetentionPolicy.RUNTIME,要不然反射机制拿不到
@Retention(RetentionPolicy.RUNTIME)
public @interface GJQ {
String[] hobbies();
}
反射获取注解的属性值
@GJQ(hobbies={"basketball","football"})
public class AnnDemo {
public static void main(String[] args) throws Exception{
Class aClass = Class.forName("com.gjq.Annotation.AnnDemo");
if(aClass.isAnnotationPresent(GJQ.class)){//判断是否被GJQ注解修饰
GJQ GJQannotation =(GJQ) aClass.getAnnotation(GJQ.class);如果被GJQ修饰的话就获取这个注解对象强转成GJQ注解
String[] ss=GJQannotation.hobbies();//获取类上注解的值
for (String s : ss) {//遍历属性值
System.out.println(s);
}
}
}
}
注解在开发当中有什么用??
假设有一个@ID注解,只能修饰在类上面,表示被这个注解修饰的必须有一个int类型的id属性
代码如下
package com.gjq;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
public class finalDmeo {
public static void main(String[] args)throws Exception {
Class<?> aClass = Class.forName("com.gjq.User");
if(aClass.isAnnotationPresent(ID.class)){
boolean isOK=false;//判断是否包含int的id属性的标志
Field[] fields = aClass.getDeclaredFields();//反射获取类中的所有属性
for (Field field : fields) {//遍历属性
if((field.getName().equals("id"))&&(field.getType().getSimpleName().equals("int"))){//如果有属性名是id,并且为int类型,就isOK赋值为TRUE
isOK=true;
break;
}
}
if(!isOK){如果为TRUE,说明没有int类型的id属性
throw new HasNotIdException("没有ID属性异常");
}
}
}
}
@Retention(RetentionPolicy.RUNTIME)
@interface ID{
}
@ID
class User{
private int idd;
private String name;
}
class HasNotIdException extends RuntimeException{
public HasNotIdException() {
}
public HasNotIdException(String message) {
super(message);
}
}