Java基础
Java开发六大原则
- 单一原则:一个类或者一个方法只负责一个事情。
- 里斯替换原则:子类不应该重写父类已实现的方法,重载不应该比父类的参数更少。
- 依赖倒置原则:面向接口编程。(更加能够增加程序的扩展性)
- 接口隔离原则:接口中的方法应该细分,要合理的隔离开不同功能到不同的接口中。
- 迪米特原则:高内聚低耦合。
- 开闭原则:对修改关闭,对扩展开放。
基础语法
一个字符占字节数量?
- 一个字符占1个字节(GBK、ASCII、UTF-8)
- 一个中文占2个字符(GBK、ASCII)
- 一个中文占3个字节(UTF-8)
值传递和引用传递
Java是值传递语言
值传递
定义:方法被调用的时候,实参通过形参把它的内容复制了一份传入方法内部,此时形参所拿到的值只是实参的副本。所以方法不会改变实参的数据
引用传递
定义:在方法调用时,实参的地址通过方法调用被传递给相应的形参,在方法体内,形参和实参指向同一块内存地址,对形参操作会影响真实的内容。
四大引用
引用类型 | 回收时机 | 使用场景 |
---|---|---|
强引用 | 不回收 | 创建对象实例 |
软引用 | 内存不足时 | 图片缓存 |
弱引用 | 垃圾回收 | WeakHashMap,维护一种非强制的映射关系 |
虚引用 | Unknow | 跟踪对象垃圾回收的活动 |
关键字定义
static
- static可以用来修饰成员变量和方法,static用在没有创建对象的情况下调用方法/变量
- static是类变量与类的生命周期相同
在一个静态方法内调用一个非静态成员为什么是非法的?
答:这个需要结合 JVM 的相关知识,静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,然后通过类的实例对象去访问。在类的非静态成员不存在的时候静态成员就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
final
- 被修饰的类不能被继承。
- final修饰的方法,不能被任何子类重写
- final修饰变量分为两种情况,一种是修饰基本数据类型,表示数据类型的值不能被修改;一种是修饰引用类型,表示对其初始化之后便不能再让其指向另一个对象
volatile
保证了变量的可见性(visibility)。被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象。
序列化
静态成员不保存序列化
使用 Java 对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装 成对象。必须注意地是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对 象序列化不会关注类中的静态变量。
序列化用户远程对象输出
除了在持久化对象时会用到对象序列化之外,当使用 RMI(远程方法调用),或在网络中传递对象时, 都会用到对象序列化。Java序列化API为处理对象序列化提供了一个标准机制,该API简单易用
Serializable 实现序列化
在 Java 中,只要一个类实现了 java.io.Serializable 接口,那么它就可以被序列化。
ObjectOutputStream 和 ObjectInputStream 对对象进行序列化及反序列化
通过 ObjectOutputStream 和 ObjectInputStream 对对象进行序列化及反序列化。
writeObject 和 readObject 自定义序列化策略
在类中增加 writeObject 和 readObject 方法可以实现自定义序列化策略。
序列化 ID
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个 类的序列化 ID 是否一致(就是 private static final long serialVersionUID)
Transient 关键字阻止该变量被序列化到文件中
- 在变量声明前加上 Transient 关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
- 服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串 等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在 客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的 数据安全。
访问权限
private | default | protected | public | |
---|---|---|---|---|
同一类 | ok | ok | ok | ok |
同一个包 | ok | ok | ok | |
子类 | ok | ok | ||
其他包中的类 | ok |
面试知识点
equals定义
定义:equals是Object里面的方法,默认比较的是两个对象的地址
封装类对equals进行了重写,主要是为了进行值的对比,而不是地址
例如String的equals()
1 | public boolean equals(Object anObject) { |
==和equals的区别?及注意事项
- ==:判断的两个字符串在内存的地址是否相同
- equals:没有重写的时候判断的内存地址,重写之后进行了值的比较
注意事项
- null.equals(“wu”)会报错,最好的写法是“字符串”.equals(对象)
equals与hashCode()的关系
- equals相等的对象则他们的hashCode一定相等,hashCode相等的对象equals不一定相等
- hashCode作用是为了提高散列结构存储中查找的效率
- java中重写equals时尽量重写hashCode方法:声明相等对象必须要有相等的hashCode.
substring中JDK1.6和JDK1.7的区别
答:在1.6的时候是存在内存泄露问题。因为在1.6的时候substring和string指向的是内存里面同一个char数组,然而substring一直引用着这个数组,GC垃圾回收机制不会回收。当string很大的时候就会产生内存泄露问题。解决办法是在substring过后拼接一个空字符串使其重新在内存中生成一个新的char数组。这样就不会和string产生关联性,则可以被回收。
什么是序列化?为什么要序列化?
定义:序列化,有称为“串化”,把java对象内存中的数据编成一串二进制的数据,然后把这些数据放在可持久化的设备上面,如磁盘。需要数据时通过反序列化可以把对象还原到内存中。
序列化的好处:
- 方便数据传递,减少数据丢失率,增强程序安全性
- 有利于数据存储,减少不必要的内存浪费,节约了资源
- 简化了数据库结构,增强了程序的可维护性
String、StrinBuilder和StringBuffer的区别
- String是final修饰的,不可变,每次操作都会产生一个新的对象
- StringBuilder和StringBuffer都是在原对象上操作
- StringBuffer是线程安全的内部被synchronized修饰
- StringBuilder不是线程安全
性能:StringBuilder > StringBuffer > String
面向对象
面向对象三大特性
封装:是面向对象方法的重要原则,就是把对象的属性和操作(或服务)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节。
优点:
- 将变化隔离
- 便于使用
- 提高重用性
- 提高安全性
缺点:
- 将变量等使用private修饰,或者封装方法内,使其不能直接被访问,增加了访问步骤和难度
继承:继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力。关键字(extends)让类与类之间产生继承关系。
优点:
- 减少代码量,能很好的提高复用率
- 使类与类之间存在继承关系,是实现多态操作的前提
缺点:
- 继承使得多个类之间具有了子父类关系,当一个类存在多个子类的时候,如果父类发生变化,那么这些子类会跟着一同变化,造成类与类之间的“强耦合”关系!
多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。简单的说:就是用基类的引用指向子类的对象。
多态存在的前提:
- 必须有子类和父类,具有继承或实现(继承)
- 子类必须重写父类的方法(重写)
- 父类的引用变量指向子类的对象(向上转型)
优点:
- 可替换性,多态对一存在的代码具有可替代性
- 可扩充性:增加的子类不影响已存在的类的特性的运行和操作
- 接口性:多态时超类通过方法签名想子类提供了一个公共的接口,由子类来完善或者覆盖它而实现的
- 灵活性:在应用中体现了灵活多样的操作,提高了使用的效率
- 简化性: 多态简化对应用软件的代码的编写和修改过程,尤其在处理大量的对象的运算和操作时,这个特点尤为突出和重要
缺点:
- 只能使用父类的引用访问父类的成员
super和this的区别
- super()和this()类似,区别是,super()从子类中调用父类的构造方法,this()在同一类内调用其它方法。
- super()和this()均需放在构造方法内第一行。
- 尽管可以用this调用一个构造器,但却不能调用两个。
- this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
- this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。
- 从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。
内部类
成员内部类
1 | public class Test10 { |
特点:成员内部类可以无条件访问外部类的属性和方法,但是外部类想要访问内部类属性或方法时,必须要创建一个内部类对象,然后通过该对象访问内部类的属性或方法
成员内部类的访问权限
- private:仅外部类可以访问
- protected:同包下或继承类可以访问
- default:同包下可访问
- public:所有类可访问
局部内部类
1 | class K{ |
特点:局部内部类存在于方法中。他和成员内部类的区别在于局部内部类的访问权限仅限于方法和作用域内。
注意事项:局部内部类就像局部变量一样,前面不能访问修饰符以及static修饰符。
匿名内部类
1 | public class Test13 { |
特点:分析以上代码知道静态方法driveCar需要一个Car对象,我们通过实现接口创建一个匿名类对象传递过去。事实上还可以通过继承类来创建一个匿名内部类对象。
注意事项:匿名内部类没有构造方法。也是唯一没有构造方法的内部类。匿名内部类和局部内部类只能访问外部类的final变量。
静态内部类
1 | class U { |
特点:静态内部类和成员内部类相比多了一个static修饰符。它与类的静态成员变量一般,是不依赖于外部类的。同时静态内部类也有它的特殊性。因为外部类加载时只会加载静态域,所以静态内部类不能使用外部类的非静态变量与方法。其他类使用静态内部类需要使用“外部类.静态内部类”方式,如下所示:Out.Inner inner = new Out.Inner();
内部类的好处
- 完善了Java多继承机制,由于每一个内部类都可以独立的继承接口或类,所以无论外部类是否继承或实现了某个类或接口,对于内部类没有影响。
- 方便写事件驱动程序。
重写与重载
重载: 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
重写: 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方法。
重写要遵从“两同两小一大”
- “两同”即方法名相同、形参列表相同;
- “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
- “一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。
初始化顺序
- 静态属性:static 开头定义的属性
- 静态方法块:static{} 包起来的代码块
- 普通属性:非static定义的属性
- 普通方法块:{}包起来的代码块
- 构造函数:类名相同的方法
- 方法:普通方法
接口与抽象类
抽象类和接口的对比
参数 | 抽象类 | 接口 |
---|---|---|
默认的方法实现 | 可以有默认的方法实现 | 接口完全是抽象的。根本不存在方法的实现 |
实现 | 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,他需要提供抽象类中所有声明的方法的实现 | 子类使用关键字implements来实现接口。它需要提供接口中所有声明的方法的实现 |
构造器 | 抽象类可以有构造器 | 接口不能有构造器 |
与正常Java类的区别 | 除了不能实例化抽象类之外,它和普通Java类没有任何区别 | 接口完全不是同一个类型 |
访问修饰符 | 抽象方法可以有public、protected和default这些修饰符 | 接口方法默认修饰符是public。不能使用其它的修饰符 |
main方法 | 抽象类可以有main方法 | 接口没有main方法 |
多继承 | 抽象方法可以继承一个类和实现多个接口 | 接口只可以继承一个或多个其它接口 |
速度 | 比接口速度快 | 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法。 |
添加新方法 | 如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。 | 如果你往接口中添加方法,那么你必须改变实现该接口的类。 |
接口特点
- interface接口是一个完全抽象的类,他不会提供任何方法的实现,只会进行方法的定义
- 接口中只有两种访问修饰符,一种是public,另外一种是default
- 接口只提供方法的定义,接口没有实现,但是接口可以被其他类实现。
- 接口不能被实例化,所以接口不能有任何的构造方法
抽象类特点
- 如果一个类中有抽象方法,那么这个类一定是一个抽象类
- 抽象类中不一定只有抽象方法,也可以有具体的方法,你可以自己去选择是否实现这些方法
- 抽象类中的约束不像接口那么严格,你可以在抽象类中定义构造方法,抽象方法,普通属性,方法,静态属性和静态方法
- 抽象类和接口一样不能被实例化,实例化只能实例化具体的类
集合
框架

集合框架

Iterable 接口
jdk1.8以后有了for-each方法
1 | default void forEach(Consumer<? super T> action) { |
jdk 1.8之前Iterator只有iterator一个方法
1 | Iterator<T> iterator(); |
使用迭代器的方式进行遍历
1 | for(Iterator it = coll.iterator(); it.hasNext(); ){ |
List
ArrayList
描述:底层是数组结构,可重复。线程不安全,因为数组结构所以是有序的。
特点:
ArrayList
实现了Cloneable
接口 ,即覆盖了函数clone()
,能被克隆RandomAccess
是一个标志接口,表明实现这个这个接口的 List 集合是支持快速随机访问的。在ArrayList
中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。ArrayList
实现了java.io.Serializable
接口,这意味着ArrayList
支持序列化,能通过序列化去传输。
扩容机制:在初始化集合时数组长度是0,当进行插入操作时才分配容量为10。(当前容量+1)*1.5。
Vector
描述:没啥描述的几乎不用了。
LinkedList
描述:有序可重复,线程不安全,底层结构是双向链表(jdk1.6之前是循环链表,1.7就取消了循环),同时LinkedList实现了Deque接口,使得能够进行队操作。
实现线程安全方式:
1 | List list=Collections.synchronizedList(new LinkedList(...)); |
Queue
Deque
描述:被LinkedList和ArrayQueue实现。双端队列
LinkedList与ArrayQueue区别
- LinkedList是基于双向链表实现的,ArrayQueue是基于数组实现。
- PriorityQueue可以作为堆使用,而且可以根据传入的Comparator实现大小的调整,会是一个很好的选择。
- ArrayDeque通常作为栈或队列使用,但是栈的效率不如LinkedList高。
- LinkedList通常作为栈或队列使用,但是队列的效率不如ArrayQueue高。
- 在java中,Queue被定义成单端队列使用,Deque被定义成双端队列使用。而由于双端队列的定义,Deque可以作为栈或者队列使用,而Queue只能作为队列或者依赖于子类的实现作为堆使用。
PriorityQueue
描述:PriorityQueue的底层数据结构是数组,而无边界的形容,那么指明了PriorityQueue是自带扩容机制的。可以看到插入元素的时候是需要经过compareTo的处理,那么最常用就是一些范围极值的输出,类似于堆排序的用法。
Set
描述:值不能重复。对象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的此序号)判断 的,如果想要让两个不同的对象视为相等的,就必须覆盖 Object的hashCode方法和equals方法。
AbstractSet实现了对equals和hashCode的重写
HashSet
描述:无序,值唯一,线程不安全,存取速度快,底层基于Hash表实现。HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较 equals 方法 如果 equls结果为true ,HashSet就视为同一个元素。如果 equals为false就不是同一个元素。
实现线程安全方式:
1 | Set s = Collections.synchronizedSet(new HashSet(...)); |
TreeSet
描述:有序,不可重复,结构是红黑树(自平衡的排序二叉树),线程不安全
- TreeSet()是使用二叉树的原理对新 add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。
- Integer 和 String 对象都可以进行默认的 TreeSet 排序,而自定义类的对象是不可以的,自己定义的类必须实现 Comparable 接口,并且覆写相应的 compareTo()函数,才可以正常使用。
- 在覆写 compare()函数时,要返回相应的值才能使 TreeSet 按照一定的规则来排序
- 比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。
LinkedHashSet
描述:HashSet的子集,内部是LinkedHashMap实现。
区别:它维护一个贯穿其所有条目的双链接列表。这个链表定义了迭代顺序,即元素插入集合的顺序(插入顺序)。请注意,如果将元素重新插入到集合中,则插入顺序不受影响。
Map
HashMap
描述:线程不安全,键不能重复,值可以重复。键值都可以为null
实现线程安全方式:
1 | Map m = Collections.synchronizedMap(new HashMap(...)); |
扩容机制:HashMap
默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。创建时如果给定了容量初始值HashMap
会将其扩充为 2 的幂次方大小(HashMap
中的tableSizeFor()
方法保证)。也就是说 HashMap
总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
JDK1.8 之前 HashMap
底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
JDK 1.8 HashMap 的 hash 方法源码:
JDK 1.8 的 hash 方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。
1 | static final int hash(Object key) { |
对比一下 JDK1.7 的 HashMap 的 hash 方法源码.
1 | static int hash(int h) { |
JDK 1.8比JDK 1.7在拉链法上面多了一层红黑树来加快查询速度
如果在解决哈希冲突时,链表长度大于阈值8,如果数组长度小于64则先对数组进行扩容。之后再进行红黑树转换
HashMap的长度为什么是2的幂次方
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash
”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。
loadFactor加载因子
loadFactor 加载因子是控制数组存放数据的疏密程度,loadFactor 越趋近于 1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor 越小,也就是趋近于 0,数组中存放的数据(entry)也就越少,也就越稀疏。
threshold
threshold = capacity * loadFactor,当 Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准。
LinkedHashMap
描述:继承HashMap线程不安全。具体逻辑和LinkedHashSet一样
HashTable
描述:数组加链表结构,线程安全,现在也不怎么用
TreeMap
描述:底层是二叉树实现,键不能为null,有序。
ConcurrentHashMap
Java7 中 ConcurrentHashMap 使用的分段锁,也就是每一个 Segment 上同时只有一个线程可以操作,每一个 Segment 都是一个类似 HashMap 数组的结构,它可以扩容,它的冲突会转化为链表。但是 Segment 的个数一但初始化就不能改变。
Java8 中的 ConcurrentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构也由 Java7 中的 Segment 数组 + HashEntry 数组 + 链表 进化成了 Node 数组 + 链表 / 红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。
有些同学可能对 Synchronized 的性能存在疑问,其实 Synchronized 锁自从引入锁升级策略后,性能不再是问题,有兴趣的同学可以自己了解下 Synchronized 的锁升级。
1.7和1.8的区别
jdk7:数据结构:ReentrantLock+Segment+HashEntry,一个Segment中包含一个HashEntry数组,每个 HashEntry又是一个链表结构 元素查询:二次hash,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部 锁:Segment分段锁 Segment继承了ReentrantLock,锁定操作的Segment,其他的Segment不受影 响,并发度为segment个数,可以通过构造函数指定,数组扩容不会影响其他的segment get方法无需加锁,volatile保证
jdk8: 数据结构:synchronized+CAS+Node+红黑树,Node的val和next都用volatile修饰,保证可见性 查找,替换,赋值操作都使用CAS 锁:锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容读操作无锁: Node的val和next使用volatile修饰,读写线程对该变量互相可见 数组用volatile修饰,保证扩容时被读线程感知
Arrays.sort()原理分析
Java Arrays中提供了两种排序方式:
- 基本类型:采用调优的快速排序
- 对象类型:采用改进的归并排序,及快速又稳定,对象数组中保存的只是对象的引用,这样多次移位并不会造成额外的开销,但是,对象数组对比较次数一般比较敏感,有可能对象的比较比单纯数的比较开销大很多。归并排序在这方面比快速排序做得更好,这也是选择它作为对象排序的一个重要原因之一。
排序优化:实现中快排和归并都采用递归方式,而在递归的底层,也就是待排序的数组长度小于7时,直接使用冒泡排序,而不再递归下去.
异常
Java异常层次结构图
Error:Error类对象是由Java虚拟机生成并抛出,Error表示编译时和系统错误,通常不能预期和恢复,不如硬件故障、JVM崩溃、内存不足等。
Exception:在Exception分支中有一个重要的子类RuntimeException(运行时异常),该类型的异常自动为你所编写的程序定义ArrayIndexOutOfBoundsException(数组下标越界)、NullPointerException(空指针异常)、ArithmeticException(算术异常)、MissingResourceException(丢失资源)、ClassNotFoundException(找不到类)等异常,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。
throw和throws区别
throw:(针对对象的做法)抛出一个异常,可以是系统定义的,也可以是自己定义的
throws:(针对一个方法抛出的异常)抛出一个异常,可以是系统定义的,也可以是自己定义的
- throws出现在方法函数头;而throw出现在函数体。
- throws表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某种异常。
- 两者都是消极处理异常的方式(这里的消极并不是说这种方式不好),只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。
泛型
什么是泛型
定义:泛型的本质就是“参数化类型”,所操作的数据类型被指定为一个参数。在创建集合的时候指定集合元素的数据类型。该集合只保存指定类型的元素。
类型擦除
定义:java编译器生成的字节码是不包含泛型信息的。所以泛型类型在编译的时候被擦除,这个过程叫做类型擦除
。类型擦除相当于吧泛型java代码变成普通java代码。
类型擦除的主要过程如下
1. 将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
2. 移除所有的类型参数。
extends T>表示该通配符代表的类型是T类型的子类 super T>表示该通配符代表的类型是T类型的父类
反射
定义:反射就是动态加载对象,并对对象进行剖析。在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法,这种动态获取信息以及动态调用对象方法的功能成为Java反射机制。
1 | Class<?> clz = Class.forName("fs.Student"); |
反射的优缺点:
优点:
- 反射提高了程序的灵活性和扩展性,底层框架用的比较多。
缺点:
- 性能低。反射包括了一些动态类型,所以JVM无法对这些代码进行优化。所以效率低
- 安全限制低。反射是要求程序在一个没有安全限制的环境中运行的
- 由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方 法),所以使用反射可能会导致意料之外的副作用--代码有功能上的错误,降低可移植性。 反射代码破坏了抽象性。
编译时类型和运行时类型
1 | Person p = new Student(); |
其中编译时类型为Person,运行时类型为Student。
反射 API 用来生成 JVM 中的类、接口或则对象的信息。
- Class 类:反射的核心类,可以获取类的属性,方法等信息。
- Field 类:Java.lang.reflec 包中的类,表示类的成员变量,可以用来获取和设置类之中的属性 值。
- Method 类: Java.lang.reflec 包中的类,表示类的方法,它可以用来获取类中的方法信息或 者执行方法。
- Constructor 类: Java.lang.reflec 包中的类,表示类的构造方法。
用法示例
1 | //获取 Person 类的 Class 对象 |
注解
元注解(4个)
- @Target - 作用域
- ElementType.TYPE 用于描述类、接口或enum声明
- ElementType.FIELD 用于描述实例变量
- ElementType.METHOD 方法声明
- ElementType.PARAMETER 参数
- ElementType.CONSTRUCTOR 构造器
- ElementType.LOCAL_VARIABLE 局部变量
- ElementType.ANNOTATION_TYPE 另一个注释
- ElementType.PACKAGE 包
- @Retention 生命周期,定义了该Annotation被保留的时间长短
- RetentionPolicy.SOURCE – 在源文件中有效(即源文件保留)
- RetentionPolicy.CLASS – 在class文件中有效(即class保留)
- RetentionPolicy.RUNTIME– 在运行时有效(即运行时保留)
- @Documented 是否生成javadoc文档。
- @Inherited 是否被子类继承
自定义注解
定义:使用@interface自定义注解时,自动继承了java.lang.annotation.Annotation接口,由编译程序自动完成其他细节。在定义注解时,不能继承其他的注解或接口。 @interface用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。可以通过default来声明参数的默认值。
I/O
- 本文作者: GHOSTLaycoo
- 本文链接: http://example.com/2021/11/16/java基础/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!