文章目录
- JAVA SE
- 抽象类与其实现子类
- 抽象类与其子类如下
- 静态函数不存在重写和多态的概念
- 重写的要求
- 接口interface
- 类-接口的实现(一对多)、接口-接口的继承(一对多)
- 接口中的变量和函数
- 接口作为一种标签
- 堆、栈、静态方法区
- 堆heap
- 栈stack
- 静态方法区method
- 垃圾回收
- 克隆clone()
- Collection家族中的Clone()
- 传参
- 可变长的参数
- 数组及其初始化
- 引用数据类型
- 基本数据类型
- 二维数组
- 静态初始化和动态初始化
- 静态初始化
- 动态初始化
- 集合Collection
- Collection2Array
- .toArray()
- .toArray(T[] a)
- Arrays类和Collections类
- Arrays
- 排序
- 填充
- Array2Collection
- Collections
- 迭代器Iterator
- ==和equals()和hashCode()
- ==比较的是变量本身的值
- 装箱和拆箱
- 什么是装箱
- 装箱缓存
- 重写equals()可以丰富“相等”的含义
- 如果重写equals(),也需要重写hashCode()
- 重写equals的常见格式
- 为什么如果重写equals(),也需要重写hashCode()?
- hashcode的应用
- hashset
- hashmap
- 泛型Generic
- 泛型方法
- 泛型类
- 泛型通配符和泛型类型限制
- IO流
- 控制台io
- 字节流
- FileInputStream
- BufferedInputStream
- FileOutputStream
- BufferedOutputStream
- 字符流
- FileReader
- BufferedReader
- FileWriter
- BufferedWriter
- 序列化
- serializable接口
- ObjectInputStream
- ObjectOutputStream
- 多线程multiThread
- 继承Thread类
- 实现runnable接口
- 匿名内部类方式实现runnable接口
- 静态代理模式
- 代理的作用
- 代理类要求
- 线程不安全问题
- 注释Comment
- 注解Annotation
- 1 生成文档相关的注解
- 2 编译过程中的格式检查
- 3 跟踪代码依赖性,实现替代配置文件的功能
- 自定义注解和元注解
- 自定义注解@interface
- 元注解
- 注解测试
- 反射reflection
- 理解反射
- 反射的提出背景
- 反射机制有什么用?
- 类的加载过程
- 万物皆是对象
- 类的加载时机
- 加载流程
- Class类的特点
- 代码示例
- 代码实例的解析(重点)
- 反射的动态性的应用
- 反射是否破坏了封装性
- 获取Class对象的几种方法
- 用`ClassLoader`获取文件输入流
- 一些方法实例
- `clazz.getFields()`
- `clazz.getDeclaredFields()`
- `clazz.getMethods`
- `clazz.getDeclaredMethods`
- 体会架构中通过反射获取注解的过程
- 属性文件Properties
- JavaBean
JAVA SE
学习方法:菜鸟java(基础)+尚硅谷/遇见狂神说。
笔记中只有重点/难点内容,不含纯基础内容。
抽象类与其实现子类
没有函数体的方法为抽象方法。一个类中只要存在至少一个抽象方法,则这个类即为抽象类。抽象类不能被实例化。抽象方法和抽象类用abstract关键字修饰。
抽象类与其子类如下
package basicGrammer;
public abstract class SuperClass{
public abstract void m(); //抽象方法
public int getInt(){return 1;}
public static String staticFunction(){
return "SuperClass's static function";
}
}
class SubClass extends SuperClass{
//实现抽象方法
public void m(){
System.out.println("hello");
}
public static String staticFunction(){
return "SubClass's static function";
}
}
静态函数不存在重写和多态的概念
静态函数重写是没有多态作用的,因为静态的意义在于“属于类本身”而不只属于“某个对象”。不管引用变量指向的是哪个类型,静态函数不会去查找所引用对象中的复写函数,不具有多态性。其会按照引用对应的类型来调用静态函数。
SuperClass superClass = new SubClass();
SubClass subClass = new SubClass();
superClass.staticFunction()//"SuperClass's static function"
subClass.staticFunction()//"SubClass's static function"
重写的要求
重写的基本要求是:
- 只能重写从父类或接口继承过来的函数。父类的private的函数、final的函数、static的函数无法被重写。
- 函数的签名一致。即:函数名+参数列表完全一致。
- 返回值要兼容。
- 访问权限不能更加严格。
- 不能抛出比父类中更高级别的异常。
- 构造方法不能被重写
- synchronized 和 strictfp 关键字对重写规则没有任何影响。
所有要求的基本逻辑是:在扩充了更多的子类后,将其new的对象赋给父类的引用变量后,不能影响其余的程序段的运行。设想如果加了一个子类,重写了父类的函数后,出现了返回值赋值错误、抛出异常未能被接受的错误、访问权限受限错误等等,则在整个项目的各处都会报错。所以重写函数一定要注意以上几点。
Father p = new son1();//以往的程序
Father p = new son2();
Father p = new son3();
//多态扩充后
Father p1 = new anotherSon();
接口interface
类-接口的实现(一对多)、接口-接口的继承(一对多)
java中不允许多继承(一个类有多个父类),是为了防止父类1和父类2中有同名冲突的方法,在子类调用中会出现冲突。但是可以实现多个接口,且接口间允许多继承。是由于即使有同名方法也没关系,因为这几个接口中的重复的方法声明对应于实现类中的同一个方法实现。
public interface super1 {
}
public interface super2 {
}
public interface Interface2 extends super1,super2{
public static final int interfaceNum=2;
}
public interface Interface {
}
public class Employee{
}
public class Salary extends Employee implements Interface,Interface2{
}
接口中的变量和函数
接口定义了一种协议和规则,所以应该:
- 成员变量是:
- 公开的:不公开则毫无意义,凡是实现该接口的都应该可以访问该成员。
- 静态的:接口不能被实例化,所以变量都必须是放在静态存储区的。
- 不可改变的:因为所有人都应该遵守,不能随便改。
- 函数是:
- 公开的
- 抽象的
public interface Interface {
public static final int interfaceNum=1;
public abstract int func1();
}
简写成:
public interface Interface {
int interfaceNum=1;
int func1();
}
注:JDK 1.8 以后,接口里可以有静态方法和方法体了。JDK 1.8 以后,接口允许包含具体实现的方法,该方法称为"默认方法",默认方法使用 default 关键字修饰。更多内容可参考 Java 8 默认方法。但是这会引起实现类中的同名函数冲突,模糊了接口和抽象类的边界。
public interface Interface {
public static final int interfaceNum=1;
public int func1();
public static int func2(){
return 0;
}
public default int func3(){
return 2;
}
}
如果出现冲突,则需要在子类中指定使用哪一个接口中的静态或默认方法。
public class C implements interfaceA,interfaceB{//假设A和B中定义了同签名的default show函数
@Override
public void show(String s) {
interfaceA.super.show(s);
}
}
注:JDK 1.9 以后,允许将方法定义为 private,使得某些复用的代码不会把方法暴露出去。
接口作为一种标签
public class Employee {
}
public class Salary extends Employee implements Interface,Interface2{
}
Employee n=new Employee("员工C","深圳",4);;
Employee e=new Salary("员工B","上海",2,2400);
System.out.println("n继承了接口?"+(n instanceof Interface));//false
System.out.println("e继承了接口?"+(e instanceof Interface));//true
比如java.lang.Object类的clone函数就会判断子类是否是实现了Cloneable接口来决定是否抛出not
support异常。
堆、栈、静态方法区
(42条消息) jvm内存结构(堆、方法区)_方法区是在堆里面吗_懒惰的coder的博客-CSDN博客
编译原理课上曾言,栈从大地址向小地址增长,堆从小地址向大地址增长。
堆heap
堆区只有一个,是被所有线程共享的。所以要注意线程安全问题。
堆用于存放对象实例本身。通过new
关键字创建的对象和对象数组都会被放在堆内存。
堆中的对象实例在没有被任何栈上的变量引用时,会被垃圾回收机制(GC)释放内存。
栈stack
栈存放函数运行时动态的信息,主要用于存储局部变量。每个线程都有自己的一个栈区。栈中只保存基本数据类型的对象和自定义对象的引用。每当一个方法被调用,jvm就会为该方法分配一个栈帧,如下图所示,栈帧中存放了:函数控制链(指向调用自己的函数)、函数返回值、函数参数的值,临时数据和局部变量。
静态方法区method
静态方法区存放字节码文件的类信息、字符串、常量、静态变量、静态函数的函数体等。
垃圾回收
当一个对象没有任何引用类型变量去引用它时,会被析构。
package basicGrammer;
// 一个简单的构造函数
class MyClass implements Cloneable{
int id;
// 以下是构造函数
MyClass(int id) {
System.out.println(id+" is created");
this.id=id;
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println(id+" finalize");
}
}
public class test3 extends Object{
public static void main(String[] args) throws CloneNotSupportedException {
MyClass t1 = new MyClass(10);
MyClass t2 = new MyClass(20);
t2 = null;
System.gc();
}
}
/*
10 is created
20 is created
20 finalize
*/
克隆clone()
实现Cloneable接口的类才可以调用或重写java.lang.Object中的clone()函数。这里的Cloneable接口仅仅作为一个认证标签。可以想象,在clone函数中有if this instanceof Cloneable的检查,确保子类是是实现了对应接口,才提供对应服务。但是实际上由于clone方法为native方法,即源码是通过底层的c++写的,所以这里没有查看源码验证。
但是clone方法为浅拷贝。如果实现深拷贝,需要自己override。下面给出典型案例。
package Clone;
public class demo2 {
static class Body implements Cloneable{
public Head head;
public Body(){}
public Body(Head head){
this.head=head;
}
@Override
public Object clone() throws CloneNotSupportedException {
Body copy=(Body) super.clone();
copy.head=(Head) this.head.clone();
return copy;
}
}
static class Head implements Cloneable{
public Face face;
public Head(){}
public Head(Face face){
this.face = face;
}
@Override
protected Object clone() throws CloneNotSupportedException {
Head copy=(Head) super.clone();
copy.face=(Face) this.face.clone();
return copy;
}
}
static class Face implements Cloneable{
public Face(){}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public static void main(String[] args) throws CloneNotSupportedException {
Body body1=new Body(new Head(new Face()));
Body body2=(Body) body1.clone();
System.out.println(body1==body2);
System.out.println(body1.head==body2.head);
System.out.println(body1.head.face==body2.head.face);
}
}
递归下去,所以如果想完全深拷贝,需要每个成员变量对应的类都重写了clone方法。
Collection家族中的Clone()
Collection家族中的clone()都是浅拷贝,即使内部的数据类实现了Cloneable接口。所以想要深拷贝,不仅需要内部数据元素的类重写clone,并且不能直接
ArrayList<innerClass> arrayList2=(ArrayList<innerClass>)arrayList.clone();
传参
java中的传参 都是按值传递 ,但值的内容是一个引用变量或一个基本数据类型变量。Java中是值传递的,只不过对于对象参数,值的内容是对象的引用。
- java传递引用数据类型时,按值传递。
public class test2 {
public static void main(String[] args) {
Integer a = 10;
Integer b = 20;
swap(a, b);
System.out.println(a + " " + b);
}
public static void swap(Integer a, Integer b) {
Integer c = a;
a = b;
b = c;
}
}
Integer a=10;的含义为Integer a= Integer.valueOf(10);当并不必须要在堆区new一个Integer对象时,建议用此方法。当被赋予的值在-128到127之间时,不会真的new一个Integer,而是在堆区中的cache里找到已经产生好的Integer(x)对象赋值给a。比如此时有Integer k=10;则k和a指向同一块内存地址(同一个对象的引用)。
传参后:
main中的a和b把引用的地址《按值》传递给了swap函数中的a和b。此后swap中做的操作为交换局部变量a和b的引用。使得swap中的a指向了堆中的Integer(20),swap中的b指向Integer(10)。显然,这并不能影响main中a和b的引用指向。所以不能交换a和b
- 如果在swap中操作堆中的对象,则可以影响原main函数中的结果
- java传递基本数据类型时,也按值传递。
可变长的参数
数组及其初始化
java中的变量有两种类型:基本数据类型和引用数据类型。
引用数据类型
SubClass[] sv = new SubClass[3];
for (int i = 0; i < 3; i++) {
sv[i] = new SubClass();
}
Integer[] integers=new Integer[]{1,1,3,4,5};//自动装箱
String[] strings=new String[]{"hello","hello","world"};
串池当中的字符串本质上也是一个String类型的对象。
基本数据类型
int[] array;
array = new int[]{2, 1, 3};
二维数组
String[][] s = new String[2][];
s[0] = new String[2];
s[1] = new String[3];
s[0][0] = new String("Good");
s[0][1] = new String("Luck");
s[1][0] = new String("to");
s[1][1] = new String("you");
s[1][2] = new String("!");
棕色的框框中的每个变量都指向静态方法区的串池中对应的String类对象。
静态初始化和动态初始化
静态初始化
String[][] v2d1 = {{"ab", "cd"}, {"ef", "gh"}};//静态初始化的省略格式。必须在数组变量声明时使用,如果已经声明过了则无法使用。
String[][] v2d2 = new String[][]{{"ab", "cd"}, {"ef", "gh"}};//静态初始化的完整形式
String[][] v2d3;
v2d3 = new String[][]{{"ab", "cd"}, {"ef", "gh"}};//静态初始化的拆分形式
String[][] v2d4;
v2d4 = {{"ab", "cd"}, {"ef", "gh"}};//错误
动态初始化
使用动态初始化数组时候,其中的元素将会自动拥有一个默认值:
* 如果是整数类型:那么默认为0;
* 如果是浮点数类型:那么默认为0.0;
* 如果是布尔类型:那么默认为false;
* 如果是引用类型:那么默认为null;
String[][] s = new String[2][];//用null填充
s[0] = new String[2];//用null填充
s[1] = new String[3];//用null填充
s[0][0] = new String("Good");//动态分配对象
s[0][1] = new String("Luck");
s[1][0] = new String("to");
s[1][1] = new String("you");
s[1][2] = new String("!");
集合Collection
集合部分的使用建议查看csdn博客,有很多人总结的很详细。但是结构框图普遍使用有误,这里做出修订。
Collection2Array
.toArray()
不指定接收数组。返回的是Object类型数组,每个元素都是原list中的元素的浅拷贝(直接把字面值拷贝过来)。本质:直接调用Arrays.copyOf(element Data, size)
Object[] strArray2;
strArray2=list.toArray();
for (int i = 0; i < strArray.length; i++)
{
System.out.println(strArray2[i].toString());
}
.toArray(T[] a)
如果a.length小于list元素个数就直接调用Arrays.copyOf()方法进行拷贝并且返回新数组对象,新数组中也是装的list元素对象的引用(字面值复制),否则先调用System.arraycopy()将list元素对象的引用装在a数组中,如果a数组还有剩余的空间,则在a[size]放置一个null,size就是list中元素的个数,这个null值可以使得toArray(T[] a)方法调用者可以判断null后面已经没有list元素了,然后返回a数组的引用。
String[] a = new String[list.size()];
String[] strArray=list.toArray(a);
System.out.println(strArray.hashCode());//两者哈希值相同
System.out.println(a.hashCode());
for (int i = 0; i < strArray.length; i++) //这里也可以改写为 for(String str:strArray) 这种形式
{
System.out.println(strArray[i]);
}
Arrays类和Collections类
两者都是工具类,所有成员函数都是静态的。Arrays类负责数组的操作,Collections类负责针对Collection家族的集合操作。
Arrays
排序
int[] num=new int[]{2,1,4};
Arrays.sort(array);//1,2,4
填充
String[][] s = new String[2][];
s[0] = new String[2];
s[1] = new String[3];
for (String[] i : s) {
Arrays.fill(i, "fill");
}
Array2Collection
引用数据类型数组to集合
String[] arr={"abc","cc","kkk"};
List<String> list = Arrays.asList(arr);
System.out.println(list);
//list.add("a");被禁止,java.lang.UnsupportedOperationException。因为这是由数组转过来的集合,需要保持数组“长度不可变”的特性
基本数据类型数组to集合:无法直接转
int[] num=new int[]{2,3,4};
/**
* 上面可以看到,数组的初始化使用了关键字new,而new关键字是用来初始化引用数据类型的,也就是说在Java中数组是引用数据类型。
* 这解释了上面和下面的情况,当把引用数据类型组成的数组变为集合时,会拆开,每个引用变量作为一个集合中的元素,
* 但是当把基本数据类型的数组传进去时,只认为该数组是一个引用数据类型,作为一个整体放入list中
*/
List<int[]> li =Arrays.asList(num);
Collections
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("abcd");
list.add("zz");
list.add("pp");
list.add("gg");
Collections.shuffle(list);
System.out.println(list);
Collections.fill(list,"whx");
System.out.println(list);
}
具体操作的API见jdk文档
迭代器Iterator
用遍历hashmap的例子来解释迭代器
Map<K,V>的元素是Map.Entry<K,V>
Map<String, String> map = new HashMap<String, String>();
map.put("1", "value1");
map.put("2", "value2");
map.put("3", "value3");
//方法1,用增强型for循环遍历Set
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
//方法2,用Set.iterator()返回迭代器。结合it.hasNext()和it.next()来逐个访问Set中的元素。
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
==和equals()和hashCode()
==比较的是变量本身的值
- 如果双方都是基本数据类型,则比较的是两者的值。
//基本数据类型的比较
int num1 = 10;
int num2 = 10;
System.out.println(num1 == num2); //true
- 如果有一方是基本数据类型,则进行“拆箱”,把引用类型拆为基本数据类型。
Integer num3=10;
System.out.println(num1==num3);//true
- 如果双方都是引用类型,则比较的是也是二者本身的值,只不过这个值是所引用对象的地址。一般我们可以用System.identityHashCode(myObject)来查询地址
Integer num4=10;
System.out.println(num3==num4);//true
Integer num5=200;
Integer num6=200;
System.out.println(num5==num6);//false
//比较二者本身的值(只不过比较的是被引用对象的地址,同一个对象则true,不同的对象则FALSE)
Integer a = 100;Integer b = 100;a == b 为true;因为这两个Integer变量引用的是缓存中的同一个Integer对象。
* Integer a = 200;Integer b = 200;a == b为false;是因为这两个Integer变量的引用是通过new出来的两个不同的对象。
装箱和拆箱
上面的
System.out.println(num3==num4);//true
System.out.println(num5==num6);//false
不同是因为,在执行
Integer num=10;//等价于Integer num=Integer.valueOf(10);
时,执行的是自动装箱过程。
什么是装箱
装箱:将基本数据类型转换为包装类类型
拆箱:将包装类类型转换为基本数据类型
- Java1.5 之前
Integer a = Integer.valueOf(100); //手动装箱
int b = a.intValue(); //手动拆箱
- Java1.5 之后
Integer a = 100; //自动装箱
int b = a; //自动拆箱
装箱缓存
在堆中有Integer的cache,如果运用自动装箱,当被赋予的值在-128到127之间时,这个函数不会真的new一个Integer,而是在堆区中的cache里找到已经一个产生好的Integer(x)对象赋值给num(如果没有则产生一个,但也只有一个。记住cache中同一个数字都会对应同一个对象)。所以num3和num4都是从cache中拿到的同一个Integer对象的引用。
当变量值大于等于缓存范围值时,此时底层会new Integer(),重新分配内存地址。例子中的num5和num6不在缓存范围[-128,127]内,于是从堆中new出的两个不同的Integer对象。
缓存取值范围
int、Integer (-128,127)
long、Long(-128,127)
byte、Byte(-128,127)
short、Short(-32768,32767)
char、Character(0,127)
Integer a = 100;Integer b = 100;a == b 为true;因为这两个Integer变量引用的是缓存中的同一个Integer对象。
Integer a = 200;Integer b = 200;a == b为false;是因为这两个Integer变量的引用是通过new出来的两个不同的对象。
重写equals()可以丰富“相等”的含义
Object类默认的方法为:
public boolean equals(Object obj) {
return (this == obj);
}
显然,如果不重写equals函数,则比较的依然是==,也就是二者的本身的值。这对于引用变量是不合适的。因为我们有时并不是要确定二者指向的是同一个对象(那样子用==也能判断),而是希望假如object1和object2的成员变量都一样(或者符合其他要求时),便认为二者是”相同的“,也就是object1.equals(object2)==true。所以要override这个equals函数。比如java.lang.String类中,比较两个串应该用.equals(),因为String类中重写了.equals()函数,逐个比较两个串中的字符,全都一致返回true否则false。
如果直接用静态存储区的字符串给String变量赋值,则看不出equals的作用,好似==也可以。此时s1和s2指向静态存储区的同一个String对象"hello"。
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); //true,比较地址值:内容相同,因为常量池中只有一个“hello”,所以它们的地址值相同
System.out.println(s1.equals(s2));//true,比较内容:内容相同,因为常量池中只有一个“hello”,所以它们的地址值相同
System.out.println(s1.equals("hello")); //true
但如果改为如下代码,就体现出equals的作用了。
String s3 = new String("hello");
String s4 = new String("hello"); //同上,实际上是用一个静态存储区的String对象去初始化一个自定义的Stirng对象。
System.out.println(s3 == s4); //false,比较地址值:s3和s4在堆内存中的地址值不同
System.out.println(s3.equals(s4)); //true,比较内容:内容相同
对于String s3 = new String(“world”);
首先在堆内存中申请内存存储String类型的对象,将地址值赋给s3;
在方法区的常量池中找,有无hello:
若没有,则在常量池中开辟空间存储hello,并将该空间的地址值赋给堆中存储对象的空间;
若有,则直接将hello所在空间的地址值给堆中存储对象的空间。
字符串作为最基础的数据类型,使用非常频繁,如果每次都通过 new 关键字进行创建,会耗费高昂的时间和空间代价。Java 虚拟机为了提高性能和减少内存开销,就设计了字符串常量池存放在静态方法区.
如果重写equals(),也需要重写hashCode()
重写equals的常见格式
public class bean {
Integer x;
public bean(Integer x){
this.x=x;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;//比较两个引用是否指向堆中的同一个对象,如果是则直接返回true。
if (o == null || getClass() != o.getClass()) return false;//否则,需要比较内部数据。所以当参数引用为null或者类型不匹配时,返回false。
bean bean = (bean) o;//如果是两个同类型(bean)的不同对象,则将参数对象向下转换为子类对象。
return Objects.equals(x, bean.x);//对象equals等价于内部数据equals,类似一个递归的过程。
}
@Override
public int hashCode() {//重写equals必须重写hashcode
return Objects.hash(x);//hashcode的计算保证:equal的对象必须hashcode相同。只要把equal考虑到的成员变量作为计算hashcode的参数就可以了。本例中,x相同则bean视作相同,所以hashcode由且仅由x的值唯一确定。
}
}
为什么如果重写equals(),也需要重写hashCode()?
hashCode() 方法的存在主要是用于查找的快捷性,如Hashset,HashMap等,hashCode() 方法是用来在散列存储结构中确定对象的存储地址的。虽然equals()可以比较不同的对象,但是不能插入一个元素,就从头到尾比较一轮,确保集合中没有同样的元素。所以set和map接口的实现中,选择利用散列结构来存储(也就是实现类hashset和hashmap)。先根据object.hashCode()计算出哈希值,然后直接去散列中找hash值处是否已经存放了一个对象。如果没有,则该object可以存入散列中,如果有,可能是哈希冲突导致,再利用equals来逐个判断同位置的冲突。这样子比前者的效率高了非常多。
所以,hashcode应该符合如下约定:
- 同一个对象多次调用hashCode()方法应该返回相同的值;
- 当两个对象通过equals()方法比较返回true时,这两个对象的hashCode()应该返回相等的(int)值;
- 对象中用作equals()方法比较标准的Field(成员变量(类属性)),都应该用来计算hashCode值。
- 两个对象的 hashCode 值相同,并不一定表示两个对象就相同,也就是不一定适用于 equals(java.lang.Object) 方法,只能够说明这两个对象在散列存储结构中,如 Hashtable,他们“存放在同一个篮子里”。
hashcode的应用
hashset
HashSet集合判断是否可以添加新元素标准是:先判断hashcode是否重复,若重复再判断是equals元素还是哈希冲突(不同元素但是计算得到的哈希值相同)。
如果两个元素通过equals()方法比较返回true,但是它们的hashCode()方法返回值不同,HashSet会把它们存储在不同的位置,依然可以添加成功。
hashmap
HashMap同理.HashMap,存储的数据是<key,value>对,key,value都是对象,被封装在Map.Entry中。即:每个集合元素都是Map.Entry对象。在Map集合中,判断key相等标准也是:两个key通过equals()方法比较返回true,两个key的hashCode的值也必须相等。而判断value是否相等,equal()相等即可。
泛型Generic
泛型只能用来表示引用类型,如果传递的参数是基本数据类型,则会自动装箱。
泛型方法
修饰符 < 泛型 > 返回值类型 方法名 ( 参数列表 ) {
方法体;
}
在函数返回值前加入泛型声明
public class demo1 {
public static void main(String[] args) {
Integer[] integers=new Integer[]{1,1,3,4,5};
Double[] doubles=new Double[]{1.1,1.1,3.3,4.4,5.5};
String[] strings=new String[]{"hello","hello","world"};
System.out.println( "整型数组元素为:" );
printArray( integers ); // 传递一个整型数组
System.out.println( "\n双精度型数组元素为:" );
printArray( doubles ); // 传递一个双精度型数组
System.out.println( "\n字符型数组元素为:" );
printArray( strings ); // 传递一个字符型数组
}
public static <E> void printArray(E[] input){
for (E i :input){
System.out.println(i+"\t"+System.identityHashCode(i));
}
System.out.println();
}
}
/*
整型数组元素为:
1 1625635731
1 1625635731
3 1580066828
4 491044090
5 644117698
地址相同原因:自动装箱,cache中对应同一个Integer对象。
双精度型数组元素为:
1.1 1872034366
1.1 1581781576
3.3 1725154839
4.4 1670675563
5.5 723074861
也有cache,但是可能由于浮点数的原因,没有指向同一个。(待继续研究)
字符型数组元素为:
hello 895328852
hello 895328852
world 1304836502
地址相同原因:strings[0]和strings[1]指向的是同一个静态方法区的串。
*/
泛型类
修饰符 class类名 <代表泛型的变量> extends ClassA implements InterfaceA,InterfaceB{
类成员
}
在类名后面加入泛型声明。
这个例子同时说明,泛型T只支持引用数据类型,如果给基本数据类型(基本数据类型不包含String),则会自动装箱。
public class demo2<T> extends Object implements Serializable,Cloneable {
private T t;
public void setT(T t){
this.t=t;
}
public T getT(){
return t;
}
public static void main(String[] args) {
demo2<Integer> d1=new demo2<>();
demo2<String> d2=new demo2<>();
d1.setT(4);//new Integer(4)
d2.setT("菜鸟教程");//"菜鸟教程"
System.out.println(d1.getT()+" "+d1.getT().getClass());
System.out.println(d2.getT()+" "+d2.getT().getClass());
}
}
/*
4 class java.lang.Integer
菜鸟教程 class java.lang.String
*/
泛型通配符和泛型类型限制
泛型通配符为 ?
<? extends T> 上限通配
这里?表示一个未知的类,而T是一个具体的类,在实际使用的时候T需要替换成一个具体的类,表示实例化的时候泛型参数要是T或T的子类。
<? super T> 下限通配
这里?表示一个未知的类,而T是一个具体的类,在实际使用的时候T需要替换成一个具体的类,表示实例化的时候泛型参数要是T或T的父类。
public class demo3 {
public static void main(String[] args) {
List<String> name = new ArrayList<String>();
List<Integer> age = new ArrayList<Integer>();
List<Number> number = new ArrayList<Number>();
name.add("icon");
age.add(18);
number.add(314);
getData(name);
getData2(age);
getData2(number);
getData3(age);
getData3(number);
}
//不加限制
public static <T> void getData(List<T> list){
System.out.println("getData");
for (Object i : list){
System.out.println(i);
}
}
//通配符+限制
public static void getData2(List<? extends Number> list){
System.out.println("getData2");
for (Object i : list){
System.out.println(i);
}
}
//改为泛型函数+泛型类型限制
public static <T extends Number> void getData3(List<T> list){
System.out.println("getData3");
for (T i : list){
System.out.println(i);
}
}
}
/*
getData
icon
getData2
18
getData2
314
getData3
18
getData3
314
*/
泛型通配符?的作用在于,当你不想因为List<父类> a和List<子类> b传给同一个方法func时出现类型错误,而将func改为一个泛型时:可以用泛型通配符来只 将参数改为可以接受泛型的参数。?是一个 不确定 的类型,通常用于泛型方法的调用代码和形参,不能用于定义泛型类和泛型方法。
IO流
控制台io
package basicGrammer;
import java.util.Scanner;
public class test4 {
public static void main(String[] args) {
Scanner scanner;
scanner = new Scanner(System.in);
System.out.println("nextline方式接受");
if (scanner.hasNextLine()){
String str1= scanner.nextLine();
System.out.println("输入数据为 "+str1);
}
scanner.close();
}
}
字节流
详细代码见工程。
FileInputStream
File file = new File(name);
InputStream fis = new FileInputStream(file);
// 开始读,采用byte数组,一次读取多个字节。最多读取“数组.length”个字节。
byte[] bytes = new byte[4];// 准备一个4个长度的byte数组,一次最多读取4个字节。
int readCount = 0;
// FileInputStream.read方法的返回值是:读取到的字节数量。(不是字节本身);1个字节都没有读取到返回-1(文件读到末尾)
while ((readCount = fis.read(bytes)) != -1) {
System.out.println(readCount);
// 不应该全部都转换,应该是读取了多少个字节,转换多少个。
System.out.println(new String(bytes, 0, readCount));
}
BufferedInputStream
File file=new File("src/main/java/text.txt");
InputStream inputStream=new FileInputStream(file);//节点流
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);//处理流/包装流。
int len=bufferedInputStream.available();
byte[] b=new byte[100];
bufferedInputStream.read(b);//自带缓冲区,b只作为接收数组。
String x=new String(b,0,len);
System.out.println(x);
FileOutputStream
File file=new File("src/main/java/text.txt");
OutputStream fos = new FileOutputStream(file,append=false);
byte[] bytes = {48,49,50,51,-17, -68 ,-127};
/*
utf-8编码,是变长编码。48,49,50,51是ASCII码,仅每个占用一个字节。如果首个bit为1(数字是负数),则根据不同格式可以分为两字节1字符,三字节1字符等等(类比IP地址的A、B、C三种)。这里的-17,-18,-127三个一组,表示中文字符的感叹号"!"
*/
// 将byte数组全部写出!
fos.write(bytes); // 0,1,2,3,!
// 将byte数组的一部分写出!
fos.write(bytes, 0, 2); // 再写出0,1
String s = "我骄傲!!!0123aa";
// 将字符串转换成byte数组。
byte[] bs = s.getBytes();
fos.write(bs);
fos.flush();
BufferedOutputStream
File file=new File("src/main/java/text.txt");
FileOutputStream fileOutputStream=new FileOutputStream(file,true);
BufferedOutputStream bos = new BufferedOutputStream(fos);//包装类
byte[] b={78,79,80,81};
bs.write(b);
bs.flush();
字符流
FileReader
// 创建文件字符输入流
FileReader reader = new FileReader("src/main/java/text.txt");
// 开始读
char[] chars = new char[4]; // 一次读取4个字符
int readCount = 0;
while((readCount = reader.read(chars)) != -1) {
System.out.println(readCount);
System.out.println(new String(chars,0,readCount));
}
BufferedReader
FileReader reader = new FileReader("src/main/java/text.txt");
BufferedReader br = new BufferedReader(reader);
String s = null;
while((s = br.readLine()) != null){
System.out.println(s);
}
FileWriter
FileWriter out = new FileWriter("src/main/java/text.txt");
char[] chars = {'我','是','中','国','人'};
out.write(chars);//字符数组
out.write("我是一名java软件工程师!");//字符串
out.flush();
BufferedWriter
// 带有缓冲区的字符输出流
BufferedWriter out = new BufferedWriter(new FileWriter("src/main/java/text.txt", true));
// 开始写。
out.write("hello world!");
out.write("\n");
out.write("hello kitty!");
// 刷新
out.flush();
序列化
serializable接口
想要序列化的类必须实现Serializable接口
public class bean implements Serializable{
...
}
ObjectInputStream
File file = new File("src/main/java/serializable/BinaryFile.ser");
InputStream inputStream=new FileInputStream(file);//节点流
ObjectInputStream objectInputStream=new ObjectInputStream(inputStream);//包装流
object=(bean)objectInputStream.readObject();
System.out.println(object.name+"\t"+object.id);
objectInputStream.close();
ObjectOutputStream
bean object = new bean("whx", 201992391);
File file=new File("src/main/java/serializable/BinaryFile.ser");
OutputStream outputStream=new FileOutputStream(file);//节点流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);//包装流
objectOutputStream.writeObject(object);
objectOutputStream.close();
多线程multiThread
建立一个线程,只需要新建一个Thread类对象thread即可。运行这个线程,只需要调用thread对象的start()方法即可。
而我们自定义的线程内部运行的内容,放在Thread类的run()方法中。也就是说,run()方法使我们我们不需要管理线程,只关心核心业务逻辑即可。
Runnable接口:
public interface Runnable {
public abstract void run();
}
Thread类:
class Thread implements Runnable {
private Runnable target;
public synchronized void start() {
//前处理
run();//逻辑核心
//后处理
}
@Override
public void run() {
if (target != null) {//静态代理模式
target.run();
}
}
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(String name) {
init(null, null, name, 0);
}
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
}
这里很显然,我们想要自定义run方法的内部逻辑,有以下两种办法。
继承Thread类
package multiThread;
public class thread1 extends Thread {
@Override
public void run() {
for(int i=0;i<20;i++){
System.out.println("thread 1 is running "+i);
}
}
public static void main(String[] args) {
thread1 t1=new thread1();
t1.start();
for(int i=0;i<20;i++){
System.out.println("thread main is running "+i);
}
}
}
继承Thread类后,可以重写父类的run方法以达到自定义线程运行逻辑的效果。但是不推荐,因为java单继承的要求,这个thread1类继承了Thread类后,就不能继承其他的类了。而下面的实现Runnable接口就解决了这个麻烦。
实现runnable接口
由于Thread类使用了静态代理模式,可以用一个实现了Runnable接口的类对象来初始化Thread类,成为target变量。而Thread对象中的run方法实际上调用了target的run方法。
public class runnable1 implements Runnable{
public void run(){
for(int i=0;i<20;i++){
System.out.println("thread 1 is running "+i);
}
}
public static void main(String[] args) {
runnable1 r=new runnable1();
Thread t=new Thread(r);
t.start();
for(int i=0;i<20;i++){
System.out.println("thread main is running "+i);
}
}
}
匿名内部类方式实现runnable接口
public class runnable2 {
public static void main(String[] args) {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<20;i++){
System.out.println("thread 1 is running "+i);
}
}
});
t.start();
for(int i=0;i<20;i++){
System.out.println("thread main is running "+i);
}
}
}
静态代理模式
上面提到了两个方法:一是继承Thread类,重写run方法;二是实现Runnable接口,继而赋值给Thread成为其内部的私有target变量。如果我们选择继承Thread类,则由于源码中 private Runnable target;,不能直接指定target,只能重写run函数。
而如果我们选择用一个实现了runnable接口的类来作为被代理类(Thread类是代理类,runnable1类是被代理的类),并在初始化Thread类时传给他做为target代理对象,则可以实现同样的效果。
代理的作用
避免创建大对象 通过使用一个代理小对象来代表一个真实的大对象,可以减少系统资源的消耗,对系统进行优化并提高运行速度。
举例
代理类:婚庆公司;被代理类:新婚夫妻。
代理类要求
- 代理类和目标类(被代理类)需要实现相同的接口。——相同的运作逻辑协议。
- 代理类在实现接口函数时,调用被代理类的实现函数,并丰富之。
线程不安全问题
多个线程访问临界变量时不加以约束,将会导致线程不安全问题。【未完待续】
注释Comment
//这是一个单行注释
/*
这是一个多行注释
*/
/**
* 这是一个文档注释
* the details are as follows:
* first...second....and then....
*
* @author wangh
* @version 1.2
*/
自己的文档加了文档注释后,可以用javadoc生成和java官方文档一样的html文档。
注解Annotation
1 生成文档相关的注解
在生成文档javadoc中,会体现出annotation的作用
2 编译过程中的格式检查
jdk内置的3大基本注解:
- Deprecated
- Override
- SuppressWarnings(String)
3 跟踪代码依赖性,实现替代配置文件的功能
- @WebServlet(“/login”)注解替代了如下web.xml中的配置信息
<web-app>
<servlet>
<servlet-name>TestServlet</servlet-name>
<servlet-class>com.yiyu.servlet.showAllServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>TestServlet</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
</web-app>
- spring中的关于“事务”的管理
- 单元测试 JUnit 注解@Test
package JUnitTest;
import org.junit.Test;
/*
1 maven引入JUnit依赖,并在测试java源文件加入import org.junit.Test;
2 创建单元测试类要求:
2.1 类为public
2.2 类中提供公共无参构造器
3 测试方法要求:
3.1方法的权限为public
3.2没有返回值
3.3没有形参
4 加入@Test注解
*/
public class demo1 {
@Test
public void testPrint(){
System.out.println("hello");
}
}
自定义注解和元注解
自定义的注解需要配合后续的信息处理流程(使用反射)才有意义。
框架 = 注解+反射+设计模式
通过反射可以获得注解信息
自定义注解@interface
注解只有成员变量,没有方法。注解的成员变量在注解的定义中以“无形参的方法”形式来声明,
其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。自定义注解中需要用元注解来修饰。
元注解
用来标注《注解》的《注解》。常见元注解如下:
@Retention:生存期(important)。处于RetentionPolicy.RUNTIME的注解可以在jvm中运行时通过反射机制被读取。此外还有
@Target:作用域(important)。如果不写Target元注解,则该myAnnotation注解可以在任意地方使用
@Documented:可被javadoc提取到文档,因为默认情况下javadoc是不会包含注解信息的。
@Inherited:可继承的注解,父类被myAnnotation修饰了,则子类也会被同一个annotation修饰。
@Repeatable:可重复的注解。同一个注解可以重复修饰同一个作用域。在jdk8之前,只能通过显示建立《注解数组》的方式来保存多个相同注解。该注解是在jdk8之前《数组解决方案》的基础上优化而来,加上注解后,重复的注解将自动隐式的建立《注解数组》的注解。Returns: the containing annotation type
@Target(value={ElementType.METHOD,ElementType.TYPE})//ElementType是枚举enum类型
@Retention(value= RetentionPolicy.RUNTIME)//RetentionPolicy是枚举enum类型
@Documented
@Inherited
@Repeatable(myAnnotations.class)//里面的类是这个注解的《注解数组类》。
public @interface myAnnotation {
String[] value() default "hello";//成员变量的默认值用default定义。
//如果注解没有成员,则表明作为一个标识的作用
}
@Target(value={ElementType.METHOD,ElementType.TYPE})
@Retention(value= RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface myAnnotations {
myAnnotation[] value();
}
注解测试
@myAnnotation(value = {"a","b"})
@myAnnotation //支持可重复注解,支持默认value
//上述两个重复注解等价于@myAnnotations({@myAnnotation(value = {"a","b"}),@myAnnotation })
public class demo1 {
@Deprecated
public static void print(){
System.out.println("hello");
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@myAnnotation//用default赋值,且该方法被两个注解修饰(不属于可重复注解,因为不是同一个)
@SuppressWarnings("all")
public void test(){
List<String> list=new ArrayList<>();
}
public static void main(String[] args) {
print();
}
@Test
public void testGetAnnotation(){
Class<subdemo> subdemoClass = subdemo.class;
Annotation[] annotations = subdemoClass.getAnnotations();
for(Annotation i :annotations){
System.out.println(i);
System.out.println("可见subdemo继承了父类的注解");
}
}
@myAnnotation({"a","b"})
public void test2(){}
}
class subdemo extends demo1{//继承了父类注解
}
反射reflection
理解反射
反射的特征:动态性。
原来的静态代码:将代码(字节码文件)写好后交给机器,机器就会按部就班的读取,执行。
加入反射后的动态性:程序在运行中可以反过来查看类内部的代码结构,并主动选择想要实例化的对象、选择想要执行的函数进行执行,或改变这个类的某个实例中的变量值。这实际上改变了字节码文件。
反射的提出背景
在java中会出现编译时类型 和运行时类型 不一致的问题。比如
Object obj = new String("abc");
//obj.charAt(0);报错
String x="abc";
System.out.println(x.charAt(0));//不报错
编译时类型为Object
,运行时类型为String
。但是程序想要调用运行时类型的方法,比如obj.charAt(0);
显然,这会报错。因为Object中没有getChar()
方法。此时,我们需要将obj
进行向下转型,那么应该转为什么类型呢?有以下两种方案。
- 用
instanceof
来判断。假如目前有很多不同的运行时对象都是被Object类型所引用,那么写一堆的if-else
显然是愚蠢的。
if (a instanceof String){
System.out.println(((String) a).charAt(0));
}
- 于是,我们可以通过反射
a.getClass()
,获取到对象对应的运行时类——Class对象aClass
。进而通过aClass
来得到运行时类的方法CharAt()
。最后用这个方法来反射回原本的运行时对象a
。
Object a="abc";
Class<?> aClass = a.getClass();
Method method = aClass.getMethod("charAt", int.class);
System.out.println(method.invoke(a,0));
这其实提供了一种动态性 的思想。通过反射,可以获取对象的类型。类比其他动态性语言,比如javaScript:var a = 20 ;
、scala:val a = 20
、python:a = 20
。并不需要指定引用类型,这和java中反射的思想类似,并不需要分析引用变量本身的类型来调用引用变量所指向的对象的方法和变量,而是利用反射的方法来实现。具体这些语言是如何实现动态性的,还需要进一步学习。
反射机制有什么用?
反射机制被视为是动态语言
的关键,反射机制允许程序在运行期间 借助于Reflection API取得任何类的内部信息,并直接操作任意对象的内部属性和方法。
- 反射可以获取运行时类的所有方法、所有属性、所有构造器。
- 反射可以获取运行时类的父类、接口们、包、带泛型的父类、父类的泛型、注解Annotation等
类的加载过程
万物皆是对象
加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子可以看到类的内部结构。所以我们将其形象的成为反射。
没加入反射前,对象有属性和方法,如同人可以吃,可以看,可以跑。
加入反射后,程序中的对象可以通过反射得知自己类的结构,如同照镜子一样,人照了镜子,就一下子得知,原来自己能吃是因为“有一张嘴”,能看是因为“有一双眼睛”。
类的加载时机
- 创建类的实例,也就是
new
一个对象。 - 访问类的静态方法或者静态变量(包含静态变量赋值)。
- 使用
Class.forName()
反射类或其他反射手段。 - 子类初始化的时候。
- JVM启动时标明的启动类。
加载流程
- 程序经过javac.exe【
javac Person.java
】命令后,Person.java文件被编译成一个或多个字节码文件,比如Person.class
等(Person.java
文件中可能有其他的非public外部类或者内部类,它们也会产生对应的.class文件),一个.java类对应一个.class字节码文件。 - 用java.exe命令【
java Person
】对字节码文件Person.class进行解释运行(该字节码文件中应该具有程序入口——main方法)。首先java.exe会调用底层windows的jvm.dll文件创建jvm虚拟机,根据JVM内存配置要求,为JVM申请特定大小的内存空间。 - 创建一个引导类加载器(启动类加载器)实例,初步加载系统类到内存方法区区域中;JVM申请好内存空间后,JVM会创建一个引导类加载器(
Bootstrap Classloader
)实例,引导类加载器是使用C++语言实现的,负责加载JVM虚拟机运行时所需的基本系统级别的类,如java.lang.String
,java.lang.Object
等等。引导类加载器会读取{JAVA_HOME/jre/lib/rt.jar、resources.jar 或sun.boot.class.path 路径下的内容
下的jar包和配置,然后将这些系统类加载到方法区内。 - 创建JVM 启动器实例 Launcher,并取得类加载器
ClassLoader
。然后由于Person.class字节码文件属于上述类加载时机 中的JVM启动时标明的启动类 ,于是jvm使用类加载器 将Person.class字节码文件加载到内存中的方法区(目前,方法区已成为堆区的子区域)。此过程称为类的加载。加载过程分为装载、链接、初始化三个步骤。 - 加载到方法区中的类,我们就称为运行时类 。此运行时类的产生,实际上是jvm在方法区中为之产生了一个Class类的实例化对象——Person类。后续的使用new方法实例化 、反射调用 等操作都只会通过此Class类的实例化对象——Person类来产生或执行。一个运行时类对应一个Class类的实例对象。所以说类本身也是一个对象:Class类的对象,当类被加载后,整个类的结构都会被封装在Class对象中。 且该运行时类在方法区的存储区域不会被释放,在整个执行期间,.class文件只会被jvm加载一次(不严谨)。
每一个被加载进方法区的类,比如Person类,都可以看作是一个《更高层》的类 ——Class类所产生的对象。
Class类将被加载的类中的各种成分映射成一个个的成员对象并封装在Class对象中。在Class类看来,jvm读取Person.class的过程相当于一个新的Class类的对象Person被载入到方法区中,且Person中的构造函数,成员变量,成员函数都是这个Class类的对象(Person)的成员,它们都可以通过Class类中的API获取它们。
Class类的特点
代码示例
@Test
//通过反射让person类产生对象.
// 首先,通过反射,得到Person类的Class对象,相当于得到的不是person类的对象,而是得到了person类本身。
//Class类提供了一些api来调取Person中的各个组成部分,并把他们作为一个个的java对象返回。
//然后获取该Class对象(person类本身)的构造器constructor,然后用构造器在堆中创建新的person类的对象。
public void test2() throws Exception {
Class<Person> personClass = Person.class;
System.out.println(personClass);
Constructor<Person> constructor = personClass.getConstructor(String.class, int.class);
Person tom = constructor.newInstance("Tom", 12);
System.out.println(tom);
//通过反射调用对象指定的可以访问的属性和方法。
Field age = personClass.getDeclaredField("age");
age.set(tom,10);
System.out.println(tom);
Method show = personClass.getDeclaredMethod("show");
show.invoke(tom);
//通过反射调用对象的私有的属性和方法
//通过反射用Class的对象直接new一个Instance(@deprecated)
Class clazz = Person.class;
Person per = (Person)clazz.newInstance();//不接受参数,只调用默认构造
//通过反射调用私有构造器
Constructor<Person> constructor1 = personClass.getDeclaredConstructor(String.class);
constructor1.setAccessible(true);
Person p = constructor1.newInstance("Jack");
System.out.println(p);
//通过反射直接修改私有属性
Field name = personClass.getDeclaredField("name");
name.setAccessible(true);
name.set(p,"Peter");
System.out.println(p);
//通过反射直接调用私有的方法
Method showNation = personClass.getDeclaredMethod("showNation",String.class);
showNation.setAccessible(true);
String returnString=(String) showNation.invoke(p,"China");
System.out.println("返回值是"+returnString );//如果原函数返回值为void,则这里的returnString=null
//通过反射调用类的静态属性和方法
//同过反射直接修改静态属性
Field info = personClass.getDeclaredField("info");
info.setAccessible(true);
info.set(personClass,"information field");//访问静态变量,故参数不是对象p,而是Person类。
System.out.println(info.get(personClass));
//或者参数直接写null也可以,效果同上。
info.set(null,"information field");
System.out.println(info.get(null));
//通过反射直接调用静态方法
Method showInfo = personClass.getDeclaredMethod("showInfo");
showInfo.setAccessible(true);
showInfo.invoke(null);
}
代码实例的解析(重点)
反射的动态性的应用
在web服务中,web服务器在启动的时候,会将所有的class字节码文件加载入jvm中的方法区。根据解析url来判断需要走哪个servlet,然后服务器将通过反射机制,获取方法区中对应的Class类对象,并在堆中产生所需要的servlet对象。这也是典型的反射机制的使用。仅用来产生新对象的话,没有破坏封装性。
反射是否破坏了封装性
封装性更多的是一种设计规范,虽然私有的函数(变量)和共有的函数(变量)都可以通过反射的方法获取,但是我们应该在开发中遵守这些访问的原则。就如同单例模式,只是一种设计模式,希望调用者不要自己new对象,但是通过反射依然可以做到,只不过完全没必要。综上,反射更加强大,但建议 使用者继续遵循以前的封装约束。
获取Class对象的几种方法
方式 | 备注 |
| 调用Class类的静态方法。(推荐) |
| 调用运行时类的对象的 |
| 调用运行时类的静态属性 |
| 使用类加载器(不推荐) |
public void test1() throws ClassNotFoundException {
Class<Person> personClass = Person.class;
Person person = new Person();
Class<? extends Person> aClass = person.getClass();
System.out.println(personClass==aClass);
String classname="reflection.Person";
Class<?> aClass1 = Class.forName(classname);
System.out.println(aClass1==aClass);
Class<?> aClass2 = ClassLoader.getSystemClassLoader().loadClass(classname);
System.out.println(aClass2==aClass);
}
所有的java类型都可以被Class类型的变量所指向。
用ClassLoader
获取文件输入流
用ClassLoader
获取系统类加载器SystemClassLoader
,其中的方法getResourceAsStream
可以获取文件输入流并加载配置文件(类比Properties的加载方式)
@Test
public void test3() throws IOException {
//通过类的加载器读取的文件的默认的根路径为:当前module的resources文件夹下
String filename="info.properties";
InputStream resourceAsStream = ClassLoader.getSystemClassLoader().getResourceAsStream(filename);
Properties properties = new Properties();
properties.load(resourceAsStream);
System.out.println(properties.get("user"));
System.out.println(properties.get("password"));
}
一些方法实例
代码见reflection包。
clazz.getFields()
获取所有的本类及其所有父类的public的属性
public void test3() throws ClassNotFoundException {
Class clazz = Person.class;
Field[] fields = clazz.getFields();
for (Field f:fields){
System.out.println(f);
}
}
/*
public int reflection.Person.age
public int reflection.Creature.id
*/
clazz.getDeclaredFields()
获取当前运行时类中声明的所有属性。并且有更强大、细粒度的API,可以获取每个属性的“修饰符”、“类型“、”命名“等细粒度的信息。
@Test
public void test2() throws ClassNotFoundException {
Class clazz = Person.class;
Field[] fields = clazz.getDeclaredFields();
for (Field f:fields){
System.out.println(f);
}
}
/*
private java.lang.String reflection.Person.name
public int reflection.Person.age
private static java.lang.String reflection.Person.info
*/
clazz.getMethods
类似clazz.getFields(),
获取所有的本类及其所有父类的public的方法
clazz.getDeclaredMethods
类似clazz.getDeclaredFields()
, 获取当前运行时类中声明的所有方法。同样,对于method,也有更强大、细粒度的API,可以获取每个方法的“修饰符”、“返回类型“、”命名“、“注解Annotation信息”、“抛出的异常”、“形参列表”、“包”等细粒度的信息。
@Test
public void test4() throws ClassNotFoundException {
Class clazz = Person.class;
Method[] fields = clazz.getDeclaredMethods();
for (Method f:fields){
System.out.println(f);
}
}
下面以获取方法上的注解信息为例,体会反射的作用。
//获取方法的注释信息。
@Test
public void test4() throws ClassNotFoundException {
Class clazz = Person.class;
Method[] methods = clazz.getDeclaredMethods();
for(Method m:methods){
Annotation[] annotations = m.getDeclaredAnnotations();
if(annotations.length!=0){
System.out.println(m+"\t");
}
for (Annotation a : annotations){
System.out.println(a);
}
}
}
在上述获取方法的注释信息实验中,需要注意自己定义的@myAnnotation
注解必须声明Retention为RetentionPolicy.RUNTIME
,才可以把注解信息带到运行时的内存中,才能够被反射获取 。
@myAnnotation
定义如下
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD,ElementType.FIELD,ElementType.CONSTRUCTOR,ElementType.LOCAL_VARIABLE})
public @interface myAnotation {
String value();
}
方法 | 返回值 | 备注 |
|
| 获取方法名 |
|
| 获取注释 |
|
| 返回修饰符。类似二进制的存储方式。1010表示1000(static)+10(private) |
|
| 返回形参列表 |
更多的方法见尚硅谷视频。
体会架构中通过反射获取注解的过程
注解1:表示某个类对应于mysql数据库中的表的名称
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
String value();
}
注解2:表示某个类中的成员变量对应于mysql数据库中的表的某个字段名称和类型。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
String columnName();
String columnType();
}
被注解修饰的Customer类
@Table(value = "t_costumer")
public class Customer {
@Column(columnName = "cust_name",columnType = "varchar(15)")
private String name;
@Column(columnName = "cust_age",columnType = "int")
private int age;
}
通过反射获取Customer类中各个区域的注解信息
public class test {
//获取类对应的注解
@Test
public void test1(){
Class<Customer> customerClass = Customer.class;
Table declaredAnnotation = customerClass.getDeclaredAnnotation(Table.class);
System.out.println(declaredAnnotation.value());
}
//获取属性对应的注解
@Test
public void test2() throws NoSuchFieldException {
Class<Customer> customerClass = Customer.class;
Field name = customerClass.getDeclaredField("name");
name.setAccessible(true);
Column declaredAnnotation = name.getDeclaredAnnotation(Column.class);
System.out.println(declaredAnnotation.columnName());
System.out.println(declaredAnnotation.columnType());
}
}
属性文件Properties
public void test1() throws IOException {
//根地址为module根地址
File file = new File("src/main/java/properties/info.properties");
FileInputStream fileInputStream = new FileInputStream(file);
Properties properties = new Properties();
properties.load(fileInputStream);
System.out.println(properties.get("user"));
System.out.println(properties.get("password"));
}
文件info.properties
:
user=root
password=root
JavaBean
JavaBean是一种Java语言写成的可重用组件。
所谓JavaBean,是指符合如下标准的Java类:
- 类是公共的
- 有一个无参的公共的构造器
- 原因1:子类对象实例化时,构造器首行会默认调用父类的空参构造器,除非指定。
- 原因2:在反射中,经常用于创建运行时类的对象。一般而言会使用空参构造,因为对不同的JavaBean是通用 的。故最好每个类都提供空参构造器。
- 有属性,且有对应的get、set方法
- 如果加上
toString
,equals
,hashCode
来丰富就更好了。
用户可以认为JavaBean提供了一种随时随地的复制和粘贴的功能,而不用关心任何改变。