1. 面向对象
Java是一种面向对象的编程语言,它将程序的代码和数据组织成对象。对象是具有属性和方法的实体。面向对象编程具有许多优点,例如代码复用性、可维护性和可扩展性。
2. 类和对象
类是对象的模板,它定义了对象的属性和方法。对象是类的实例,它具有类的所有属性和方法。
2.1 Object方法
编号 | 名字 | 解释 |
1 | getClass | native方法,用于返回当前对象的Class对象 |
2 | hashCOde | native方法,用于返回对象的哈希码(将对象的内存地址转换为整数返回)哈希碰撞,hashcode降低了搜索的成本。 |
3 | equals | 用于比较两个对象的地址是否一样 |
4 | clone | 用于返回当前对象的一份拷贝 |
5 | toString | 返回类的名字@实例的哈希编码的十六进制字符串 |
6 | notify | |
7 | notifyAll | 唤醒监视线程 |
8 | wait | native方法,不可以重写,暂停线程的执行 |
9 | finalize | 实例被垃圾回收的时候触发的操作 |
2.1.1 equals和hashcode
- 如果两个对象相等,则hashcode一定也是相同的
- 两个对象相等,对两个对象分别调用equals方法都返回true
- 两个对象有相同的hashcode值,它们也不一定是相等的
- 因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖(默认使用的地址来计算,不重写hashcode很可能不一样)
- hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
2.2 反射
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
类名 | 用途 | 详情 |
Class类 | 代表类的实体,在运行的Java应用程序中表示类和接口 | //第1种方式获取Class对象 :通过实例对象getClass() //第2种方式获取Class对象:类名. //第3种方式获取Class对象 Class :全类名 |
Field类 | 代表类的成员变量(成员变量也称为类的属性) | |
Method类 | 代表类的方法 | |
Constructor类 | 代表类的构造方法 | |
2.2.1 如何使用反射
(1)使用Class类,获取出被解剖的这个类的class文件对象
(2) 使用Class类方法,获取出类中的所有成员
(3) 将成员获取出来后,交给对应类,对应类中的方法,运行成员
如何获取,class文件对象
使用类的对象获取
- 每个类都使用Object作为父类,Object类方法 getClass()返回这个类的class文件对象,方法返回值Class类型对象
- 使用类的静态属性获取:类名.class 返回这个类的class文件对象.属性运行结果也是Class类型对象(并不是使用的是编译后的字节码class文件!)
- 使用Class类的静态方法获取:Class类静态方法 forName(String 类名) 传递字符串类名获取到这个类的class文件对象,方法返回值也是Class类型对象 不管用哪种方式获取的Class对象,他们都是相等的。
2.2.2 如何创建对象
(1)newInstance方法
(2)获取构造器
Constructor c=clazz.getDeclaredConstructor(String.class,String.class,int.class);
Person p1=(Person) c.newInstance("李四","男",20);
2.3 内部类
分类 | 解释 |
成员内部类 | 一个类定义在一个类的内部,看起来像类的成员。 问题:
|
局部内部类 | 定义在方法或者是一个作用域内,和成员内部类的区别在于访问权限上。 问题:
|
静态内部类 | 静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法
|
匿名内部类 | 一般使用匿名内部类的方法来编写事件监听代码。同样的,匿名内部类也是不能有访问修饰符和 static 修饰符的。 |
3. 继承、接口
继承是一种代码复用的机制,它允许子类继承父类的属性和方法。
4. 泛型
AVA泛型(Generics)是JDK 5.0引入的一个新特性,它允许在定义类、接口和方法时使用类型参数。泛型的主要目标是提供编译时的类型安全,同时减少因类型转换和装箱/拆箱带来的运行时开销。
4.1 实现原理
Java泛型的实现主要依赖于类型擦除(Type Erasure)。在编译时,泛型类型信息会被擦除,替换为原始类型(raw type),并在需要时插入类型转换。这样做的目的是为了兼容Java的旧版本,使得泛型代码能够与非泛型代码无缝集成。
4.2 使用
4.2.1 泛型类
在类名后添加类型参数。
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
4.2.2 泛型接口
public interface MyInterface<T> {
void doSomething(T t);
}
4.2.3 泛型方法
public static <T> T getFirstElement(List<T> list) {
if (list == null || list.isEmpty()) {
return null;
}
return list.get(0);
}
5. 集合框架
5.1 整体结构
以下图中的绿色的虚线代表实现,绿色实线代表接口之间的继承,蓝色实线代表类之间的继承。
5.2 常用集合
5.2.1 ArrayList
(1)实现
ArrayList
是 Java 集合框架中的一部分,它实现了 List
接口,用于存储有序的元素集合。ArrayList
是基于动态数组实现的,这意味着它使用了一个可以自动增长和缩小的数组来存储元素。
(2)扩容
编号 | 方法 | 解释 |
1 | add | 确保判断当前要加入的有空间 |
2 | ensureCapacityInternal | 和默认的大小10进行比较 |
3 | ensureExplicitCapacity | 判读是不是需要扩容,需要扩容就进行扩容 |
4 | grow | 将原来的大小变为1.5倍,看看符合要求不,不符合,就是用需求的大小,如果计算出的新的大小必最大值还大,就是用hugeCapacity |
5 | hugeCapacity | 就是使用最大的整数进行初始化 |
6 | ensureCapacity | 这个函数是提供给用户使用的,可以节约时间 |
5.2.2 LinkedList
LinkedList
是 Java 集合框架中的一部分,它实现了 List
接口和 Deque
接口,用于存储有序的元素集合。与 ArrayList
不同,LinkedList
不是基于数组实现的,而是基于双向链表实现的。这使得 LinkedList
在进行元素的插入和删除操作时具有更高的效率,特别是在列表的中间位置。
5.2.3 HashMap
-
基本概念:
HashMap
存储键值对(key-value pairs)。键(key)是唯一的,而值(value)可以重复。HashMap
不保证元素的顺序。
-
内部实现:
HashMap
内部使用哈希表(hash table)实现。每个键(key)通过哈希函数转化为一个整数,这个整数用于确定键在哈希表中的位置(桶的位置)。如果两个键的哈希值相同(哈希冲突),则它们会被放在同一个桶中的链表或红黑树中。
(1)遍历
public class HashMapTraversal {
public static void main(String[] args) {
Map<String,Integer> map=new HashMap<>();
for(int i=0;i<5;i++){
map.put("key_"+String.valueOf(i),i);
}
//(1)使用keySet
for (String key:map.keySet()){
System.out.println(key+":"+map.get(key));
}
System.out.println("-----------------------------------");
//(2)entrySet
for (Map.Entry<String,Integer> entry:map.entrySet()){
System.out.println(entry.getKey()+":"+entry.getValue());
}
System.out.println("-----------------------------------");
//(3)和(2)是一致的
Set<Map.Entry<String,Integer>> entrySet = map.entrySet();
for(Map.Entry<String,Integer> entry:entrySet){
System.out.println(entry.getKey()+":"+entry.getValue());
}
System.out.println("-----------------------------------");
//(4)使用Iterator
Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();//上下相同, 这个清晰一些
while (it.hasNext()) {
Map.Entry<String, Integer> tmp=it.next();
System.out.println(tmp.getKey()+":"+tmp.getValue());
}
}
}
(2)扩容
HashMap的扩容要和ArrayList的扩容进行区分,HashMap的扩容的真正原因是为了回避Hash冲突,当table中已经倍占用了75%(默认负载因子)就需要进行扩容,扩容的大小为原来的两倍。
- 时机:阈值=负载因子×table大小
- 扩容table
- rehash
5.2.4 ConcurrentHashMap
ConcurrentHashMap
是 Java 并发包 (java.util.concurrent) 中提供的一种线程安全的哈希表。它通过内部的分段锁机制来实现高效的并发访问。以下是 ConcurrentHashMap
加锁机制的关键点:
-
分段锁(Segment): 在
ConcurrentHashMap
的早期实现中(Java 5 和 Java 6),使用了一种称为“分段锁”的机制。ConcurrentHashMap
将整个哈希表分为多个段(Segment),每个段就像一个独立的小型哈希表。当一个线程需要对哈希表进行操作时,它只需要获取对应段的锁,而不是整个哈希表的锁。这样可以减少锁的竞争,提高并发性能。 -
锁的粒度: 由于
ConcurrentHashMap
使用了分段锁,锁的粒度比整个哈希表的锁要细。这意味着在任何给定时间,多个线程可以同时操作不同的段,从而提高了并发性能。 -
锁的实现: 在
ConcurrentHashMap
的每个段中,使用了ReentrantLock
或者ReentrantReadWriteLock
来实现锁的功能。ReentrantLock
提供了基本的互斥访问,而ReentrantReadWriteLock
则允许多个线程同时进行读操作,但写操作是互斥的。 -
Java 8 以后的改进: 从 Java 8 开始,
ConcurrentHashMap
的实现发生了变化,不再使用分段锁机制。取而代之的是,它使用了一种称为“并发级别”的机制,以及更高效的哈希表结构(如红黑树)来存储条目。Java 8 中的ConcurrentHashMap
默认是空的,并且只在需要时才创建新的条目。这种设计减少了锁的使用,因为大多数操作不需要锁定整个哈希表。 -
无锁操作:
ConcurrentHashMap
的许多操作都是无锁的,这意味着它们不依赖于显式的锁机制。相反,它们使用volatile
变量和CAS
(Compare-And-Swap) 操作来确保操作的原子性和内存可见性。 -
扩容和重哈希: 当
ConcurrentHashMap
需要扩容时,它会锁定部分哈希桶来进行重哈希操作,这样可以避免在整个哈希表上进行锁定,减少锁的竞争。
总的来说,ConcurrentHashMap
通过使用细粒度的锁和无锁操作来实现高效的并发访问。这种设计使得 ConcurrentHashMap
在多线程环境中表现出色,尤其是在读多写少的场景中。开发者在使用 ConcurrentHashMap
时,可以依赖其内部的并发控制机制,而无需担心锁的管理。
5.2.5 TreeSet,TreeMap
5.2.6 PriorityQueue
(1)实现
PriorityQueue
是 Java 集合框架中的一个类,它实现了 Queue
接口,并允许元素按照它们的自然顺序或者创建 PriorityQueue
时提供的 Comparator
进行排序。PriorityQueue
是基于优先级堆(通常是一个二叉堆)实现的。
6. 异常处理
异常处理是一种处理程序运行时错误的机制。
7. 多线程
多线程允许程序同时执行多个任务。
8. I/O
Java提供了丰富的I/O API,用于操作文件、网络和其他设备。
9. 常见问题
9.1 内存泄漏
说到垃圾回收(Garbage Collection,GC),很多人就会自然而然地把它和 Java 联系起来。在 Java 中,程序员不需要去关心内存动态分配和垃圾回收的问题,这一切都交给了JVM 来处理。顾名思义,垃圾回收就是释放垃圾占用的空间,但垃圾回收器并不是万能的,它能够处理大部分场景下的内存清理、内存泄露以及内存优化。但它也并不是万能的,不然我们在项目实践过程中也不会出现那么多的内存泄漏的问题,很多的内存泄漏都是因为开发人员操作不当导致的。
9.1.1 静态变量引用新对象
在这个例子中,StaticMemoryLeak
类的staticList
是一个静态变量,它始终持有对添加到列表中的对象的引用。如果addToList
方法被频繁调用,而列表中的元素没有被移除,那么这些元素将永远不会被垃圾回收。
public class StaticMemoryLeak {
public static List<String> staticList = new ArrayList<>();
public void addToList(String item) {
staticList.add(item);
}
}
9.1.2 单例持有外部引用
如果Singleton
类持有一个重量级对象SomeHeavyObject
的引用,并且这个单例被长时间使用,那么SomeHeavyObject
将无法被回收。
public class Singleton {
private static Singleton instance = new Singleton();
private SomeHeavyObject heavyObject;
private Singleton() {
heavyObject = new SomeHeavyObject();
}
public static Singleton getInstance() {
return instance;
}
public SomeHeavyObject getHeavyObject() {
return heavyObject;
}
}
9.1.3 ThreadLocal
变量没有被显式地移除
ThreadLocal 提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。ThreadLocal 相当于提供了一种线程隔离,将变量与线程相绑定,从而实现线程的安全,但是使用不当,就会引起内存泄露。
一旦线程不在存在,ThreadLocal 就应该被垃圾收集,而现在线程的创建都是使用线程池,线程池有线程重用的功能,因此线程就不会被垃圾回收器回收。所以使用到 ThreadLocal 来保留线程池中线程的变量副本时,ThreadLocal 没有显示的删除时,就会一直保留在内存中,不会被垃圾回收。
解决办法是不在使用 ThreadLocal 时,调用 remove() 方法,该方法删除了此变量的当前线程值。不要使用 ThreadLocal.set(null),它只是查找与当前线程关联的 Map 并将键值对设置为当前线程为 null。
try {
threadLocal.set(System.nanoTime());
}
finally {
threadLocal.remove();
}
9.1.4 不正确的 equals() 和 hashCode()
在HashMap和HashSet这种集合中,常常用到equal()和hashCode()来比较对象,如果重写不合理,将会成为潜在的内存泄露问题。
9.1.5 资源未关闭或释放导致内存泄露
当我们在程序中创建或者打开一个流或者是新建一个网络连接的时候,JVM 都会为这些资源类分配内存做缓存,常见的资源类有网络连接,数据库连接以及 IO 流。如果忘记关闭这些资源,会阻塞内存,从而导致 GC 无法进行清理。特别是当程序发生异常时,没有在finally 中进行资源关闭的情况。这些未正常关闭的连接,如果不进行处理,轻则影响程序性能,重则导致 OutOfMemoryError 异常发生。所以,最后的方式就是加上 finally,比如:
try {
//正常
} catch (Throwable t) {
//异常
} finally {
//关闭
}