大数据开发(Java面试真题)
- 一、Java基本概念和数据结构
- 1、请解释Java中HashMap和HashTable的区别?
- 2、Java中ThreadLocal的原理是什么?
- 3、请简要说明Java中equals()和hashCode()的作用及区别?
- 4、Java中的四种访问修饰符及它们之间的区别?
- 5、Java中ArrayList和LinkedList区别是什么?
- 6、Java中ArrayList扩容过程是什么?
- 7、Java中HashMap底层实现是什么?
- 8、Java中ConcurrentHashMap支持多并发的原理是什么?
- 9、HashMap原理?转换成红黑树条件?为什么这么设计?
- 10、Java线程安全的HashMap?ConcurrentHashMap和HashTable的区别?ConcurrentHashMap原理?
- 二、Java字符串和集合
- 1、请解释Java中的String、StringBuilder和StringBuffer的区别?
- 2、Java String为什么是不可变的?为什么要设计成不可变?
- 3、Java集合类型?
- 三、JVM与垃圾回收
- 1、讲一下 JVM 的垃圾回收的相关概念。
- 2、JVM 常见调优方法有哪些?
- 3、什么是JVM垃圾回收?Java中有几种垃圾回收算法?
- 4、Java的垃圾回收机制是什么?简要描述它的工作原理。
- 四、Java内存管理与性能优化
- 1、Java的内存区域分为几个部分?
- 2、什么情况下Java新建的对象不存储在Eden中?
- 3、Java线程池的核心参数有哪些?
- 4、请解释下Java中的线程池是什么,如何使用线程池来提高程序的性能?
- 五、Java异常处理和类加载
- 1、请解释Java中的异常处理机制,包括checked exception和unchecked exception?
- 2、JVM中一个类加载的过程是什么样子?
- 六、Java高级特性
- 1、Java中Synchronized的底层原理是什么?
- 2、泛型?
- 3、常用的反射方法?
- 4、Java中深拷贝和浅拷贝的区别是什么?
一、Java基本概念和数据结构
1、请解释Java中HashMap和HashTable的区别?
HashMap和HashTable是两个常见的哈希表实现类,在以下几个方面有所不同:
- 线程安全性:HashTable是线程安全的,各个方法都有synchronized修饰,适用于多线程环境。而HashMap不是线程安全的,如果多个线程同时访问一个HashMap实例并且至少一个线程修改了Map结构,则必须通过外部同步机制来保证其同步。
- 允许空键和空值:HashMap允许插入null键和null值,而HashTable不允许。
- 迭代器遍历顺序:HashMap的迭代器是fail-fast的,即在迭代过程中如果其它线程对HashMap进行结构修改(增加或者删除元素),会抛出异常。而HashTable继承自Dictionary类,并不支持迭代器。
2、Java中ThreadLocal的原理是什么?
ThreadLocal是Java中的一种线程封闭机制,它可以为每个线程都创建一个独立的变量副本,使得每个线程都可以独立地改变自己地副本,而不会影响其它线程所对应地副本==。ThreadLocal对于实现线程地局部变量以及线程安全十分有用。下面是ThreadLocal的基本工作原理:
- 内部存储:每个Thread都有一个ThreadLocalMap类型的成员变量ThreadLocals,这个变量是ThreadLocal的内部类。
- ThreadLocalMap的键为ThreadLocal对象,值为线程局部变量:也就是说,ThreadLocal本身并不存储值,它只是作为一个key来帮助线程从ThreadLocalMap获取一个线程局部变量。
- get()方法:当调用ThreadLocal的get()方法时,实际上是去调用Thread.currentThread().getThreadLocalMap().get(this)。这个操作就是获取当前线程的threadLocals成员变量,然后以当前ThreadLocal实例作为key,去获取线程局部变量。
- set()方法:当调用ThreadLocal的set()方法时,实际上是去调用Thread.currentThread().getThreadLocalMap().set(this,value)。这个操作就是获取当前线程的threadLocals成员变量,然后以当前ThreadLocal实例作为key,和将要存储的值作为value,存入ThreadLocalMap中。
3、请简要说明Java中equals()和hashCode()的作用及区别?
equals()方法判断两个对象是否相同,在Object类中默认比较两个对象是否为同一引用。而hashCode()方法返回对象对应的哈希码值,主要被哈希表使用。
作用:
-equals():主要用户判断两个对象内容是否相同(而不是引用地址),可以根据业务需求进行重写。
-hashCode():主要用于提高对对象的查找和存储效率,将对象映射为哈希表中的索引。
区别:
- equals()和hashCode()是不同目的所使用的方法,equals()用于判断对象内容相等性,hashCode()主要用于哈希表存储。
- 如果两个对象调用equals()方法返回true,则它们的hashCode值应该一样,但反之则不成立。
4、Java中的四种访问修饰符及它们之间的区别?
Java中有四种访问修饰符:public、protected、private和default(即没有明确的修饰符)。它们用于限定类、方法和变量对其它类或子类的可见性。
-public:可以被所有类访问。被public修饰的类、方法和变量都可以在任何地方访问。
-protected:只能在声明所在类或者子类中使用。被protected修饰的方法和成员变量对于同一包内其它类也是可见的。
-private:仅可以在声明所在类内部使用。私有对象无法从该类外部直接访问。
-default:没有明确指定访问级别,也称为包级私有访问权限。默认情况下,只允许同一个包内的其它类进行访问。
5、Java中ArrayList和LinkedList区别是什么?
ArrayList和LInkedList是Java中两种不同的集合类,它们的区别主要体现在以下几个方面:
- 数据结构:ArrayList是基于动态数组实现的,通过数组实现元素的存储和访问;而LinkedList则是基于双向链表实现的,通过链表节点实现元素的存储和访问。
- 插入和删除操作:ArrayList在尾部进行插入和删除操作比较高效,因为它使用数组实现,可以直接在尾部进行元素的增删;而在中间或头部进行插入和删除操作时,由于需要移动元素,效率较低。而LinkedList在任意位置进行插入和删除操作效率较高,因为它只需要更改节点的指针即可。
- 随机访问:ArrayList支持通过下标进行随机访问,可以通过索引快速定位元素;而LinkedLisk不支持直接通过下标访问,需要从头节点或尾节点开始遍历链表,直到找到对应位置的元素。
- 内存访问:ArrayList在存储元素时需要预留一定的空间,当元素数超过预留空间时,需要进行动态扩容;而LinkedList则不需要进行扩容操作,但是每个节点需要存储额外的指针信息,相对于ArrayList来说占用的内存较多。
综上所述,如果需要频繁进行插入和删除操作,并且不需要频繁随机访问元素,可以选择使用LinkedList;如果需要频繁随机访问元素,可以选择使用ArrayList。
6、Java中ArrayList扩容过程是什么?
- 在创建ArrayList对象时,默认会创建一个初始容量为10的数组。
- 当添加新元素时,如果当前数组已满(即元素个数等于数组容量),则会触发扩容操作。
- 扩容操作会创建一个新的数组,新数组的容量是原数组容量的1.5倍(JDK1.4之前为原容量的2倍)。
- 将原数组中的元素逐个复制到新数组中。
- 更新ArrayList内部的引用指向新数组。
- 新元素添加到新数组中。
7、Java中HashMap底层实现是什么?
Java中HashMap底层实现是通过哈希表(HashTable)和链表(LinkedList)结合的方式来实现的。具体来说,HashMap内部维护了一个数组,数组的每个元素是一个链表的头节点。当我们往HashMap中插入键值对时,首先会根据键的哈希值计算出在数组中的位置,然后将该键值对插入到对应链表的末尾。如果发生哈希冲突,即多个键的哈希值相同时,会将新的键值对插入到链表的头部。当链表的长度超过一定阈值(默认为8),链表就会转化为红黑树,以提高查询效率。在进行查询操作时,根据键的哈希值找到对应链表或红黑树,然后再遍历链表或搜索红黑树,找到对应值。这种底层实现方式使得HashMap再插入、删除和查找操作上都具有较高的效率。
8、Java中ConcurrentHashMap支持多并发的原理是什么?
ConcurrentHashMap是Java中线程安全的哈希表实现,它支持多并发操作的原理主要有以下几点:
- 分段锁:ConcurrentHashMap内部将数据分成多个段,每个段都可以独立地进行加锁和解锁操作。这样不同的线程可以同时访问不同的段,从而提高并发能力。
- 锁分离:与传统的同步容器不同,ConcurrentHashMap的读操作并不需要加锁,多个线程可以同时进行读操作,只要写操作需要加锁。这样可以避免不必要的阻塞,提高了并发读的效率。
- CAS(比较并交换)操作:ConcurrentHashMap使用了CAS操作来保证线程安全。在并发写操作时,通过CAS操作来实现无锁的数据更新。
- 红黑树:ConcurrentHashMap中的每个段内部使用了红黑树来存储数据,当链表长度超过阈值时,会将链表转化为红黑树,这样可以保证在最坏的情况下仍然有较高的性能。
9、HashMap原理?转换成红黑树条件?为什么这么设计?
HashMap是Java中常见的一种数据结构,它基于哈希表实现。具体原理如下:
- HashMap内部由一个数组和链表(或红黑树)组成。数组是HashMap的主体,用于存储键值对。链表和红黑树用于解决哈希冲突,提高查找效率。
- 当添加一个键值对到HashMap中时,首先根据键的hashCode()方法计算出一个哈希值,然后通过哈希值与数组长度取模,得到在数组中的位置。如果该位置上已经存在其它键值对,就发生了哈希冲突。
- 如果发生哈希冲突,会在该位置上的链表或红黑树上顺序查找键值对。如果键已经存在,则更新对应的值。如果键不存在,则在链表或红黑树的末尾添加新的键值对。
- 当链表长度超过阈值(默认为8)时,链表会转换为红黑树。
转换为红黑树的条件如下:
-当链表长度达到8时,且当前数组长度大于等于64,HashMap会将链表转换为红黑树。
-当红黑树节点数量小于等于6时,HashMap会将红黑树转换为链表。
这样设计的原因是,当链表长度过长时,查找效率会降低,因为需要遍历链表进行查找。而红黑树相比链表,具有更高效的查找、插入和删除操作,能够更好地提高HashMap的性能。同时,对于较小的链表,转换为红黑树的开销反而比链表更大,所以在节点数量小于等于6时,会将红黑树转换回链表,以节省内存空间。
10、Java线程安全的HashMap?ConcurrentHashMap和HashTable的区别?ConcurrentHashMap原理?
Java线程安全的HashMap可以使用ConcurrentHashMap来实现。ConcurrentHashMap是Java并发包中的一个线程安全的哈希表实现,它比传统的HashTable和同步的HashMap具有更好的并发性能。
ConcurrentHashMap和HashTable的区别如下:
- 锁机制:ConcurrentHashMap使用了分段锁(Segment),每个Segment相当于一个小的HashTable,只锁住当前操作的Segement而不是整个HashTable;而HashTable在每次操作时都锁住整个HashTable。
- 并发性:ConcurrentHashMap允许多个线程同时读取,而写操作会锁住相关的Segment,使得在并发写入时性能更好;HashTable在写操作时需要锁住整个HashTable,导致并发写入性能较差。
- 扩容机制:ConcurrentHashMap在扩容时只需要锁住相关的Segment,不影响其它Segment的读写操作,提高了并发性能;HashTable在扩容时需要锁住整个HashTable,导致其它线程无法读写,性能较差。
ConcurrentHashMap的原理是基于分段锁(Segment)实现的。它将整个哈希表分成多个小的HashTable(Segment),每个Segment独立地进行锁定和扩容操作。这样可以提高并发性能,允许多个线程同时读取,而写操作只需要锁定相关地Segment,不影响其它Segment的读写操作。同时ConcurrentHashMap使用CAS算法来保证并发操作的一致性和线程安全性。
二、Java字符串和集合
1、请解释Java中的String、StringBuilder和StringBuffer的区别?
String、StringBuilder和StringBuffer是Java用来操作字符串的类,在以下几个方面有所不同:
- 可变性:String类是不可变类,即String对象在创建后其值不能被改变;而StringBuilder和StringBuffer类是可变类,提供了修改和拼接字符串方法。
- 线程安全行:String是线程安全的;StringBuilder是非线程安全的;StringBuffer是线程安全但效率相对较低。
- 性能效率:由于StringBuilder不是线程安全且没有额外开销,在并发访问场景下比其它两者更高效。如果需要多个线程共享一个字符串缓存区,推荐使用StringBuffer类。
总的来说,如果需要频繁修改字符串且涉及多线程操作,应使用StringBuffer;如果是单线程操作且需要频繁修改字符串,则选择StringBuilder;而若字符串内存不需要被修改,则使用String更为合适。
2、Java String为什么是不可变的?为什么要设计成不可变?
Java中的String是不可变的,这是因为String类被设计成了不可变的对象。这意味着一旦一个String对象被创建,它的值就不能被修改。
不可变有以下几个原因:
- 线程安全:不可变的特性使得String对象在多线程环境中是安全的。因为它的值不可变,不会被其它线程修改,所以不需要同步控制。
- 缓存哈希值:String类将哈希值缓存在对象中,因为它是不可变的,所以哈希值只需要计算一次,而不需要每次使用时重新计算。这样可以提高性能。
- 字符串池:Java中的字符串池是为了节省内存而设计的。不可变的String对象可以被共享并重复使用,避免了创建多个相同值得字符串对象。这样可以减少内存占用,提高性能。
- 安全性:字符串作为参数传递给一些敏感得API时,不可变的特性可以确保参数的值不会被修改,从而保证数据的安全性。
综述所述,Java中的String被设计成不可变的主要是为了提高性能、确保线程安全以及节省内存。
3、Java集合类型?
Java集合类型有以下几种:
- List(列表):List是一个有序的集合,可以包含重复元素。常见的实现类有ArrayList和LinkedList。
- Set(集合):Set是一个不允许重复元素的集合。常见的实现类有HashSet和TreeSet。
- Map(映射):Map是一种键值对的集合,每个键只能对应一个值。常见的实现类有HashMap和TreeMap。
- Queue(队列):Queue是一种先进先出(FIFO)的集合。常见的实现类有LinkedList和PriorityQueue。
- Stack(栈):Stack是一种后进先出的集合。常见的实现类有Stack。
- Vector(向量):Vector是一个动态数组,与ArrayList类似,但是它是线程安全的。
三、JVM与垃圾回收
1、讲一下 JVM 的垃圾回收的相关概念。
Java 虚拟机的垃圾回收是 Java 内存管理的一个重要部分,它负责自动化地管理 Java 程序的内存,通过识别和回收不再使用的对象来释放内存。垃圾回收器在程序运行时进行,尽管开发者无法直接控制其精确的运行时间,但可以通过编写“内存友好”的代码以及调整 JVM 配置参数来影响其行为。
以下是关于 JVM 垃圾回收的一些基本概念:
对象的生命周期:Java 对象的生命周期开始于创建(当使用 new 关键字时)并在不再有引用指向它们时结束。如果一个对象不再被引用,那么它就可能被垃圾回收。
堆(Heap)内存:Java 对象存储在堆内存中。堆在 JVM 启动时创建,可以通过 JVM 参数调整其大小。
垃圾回收算法:垃圾回收器使用特定的算法来确定哪些对象可以被视为"垃圾"并进行回收。常见的垃圾回收算法有标记-清除、标记-压缩、复制、以及分代回收等。
停顿时间:垃圾回收器在运行时,通常会导致 Java 应用程序的执行暂停,这种现象被称为“停顿时间”。减少停顿时间是垃圾回收器优化的一个重要目标。
分代回收:Java 的垃圾回收器通常采用分代回收策略,将堆内存分为新生代和老年代。这种策略基于这样一个观察:大多数对象的生命周期都很短。
新生代:新创建的对象首先放在新生代。新生代通常分为一个 Eden 区和两个 Survivor 区(S0和 S1)。大部分对象在 Eden 区被垃圾回收。
老年代:如果对象在新生代中存活了足够长的时间,它们会被移动到老年代。老年代的空间通常比新生代大,并且其垃圾回收频率较低。
垃圾回收器:Java 提供了多种垃圾回收器,包括 Serial、Parallel、CMS、G1以及 ZGC等。每种垃圾回收器都有其特定的使用场景和优劣,选择哪种垃圾回收器取决于具体的应用需求。
2、JVM 常见调优方法有哪些?
内存分配:扩大 JVM 堆的大小可以提供更多的空间给对象,减少垃圾回收(GC)的次数。使用-Xms 和-Xmx 参数可以分别设置堆的初始大小和最大大小。然而,分配过多的内存可能会导致更长的 GC 停顿时间,并可能影响其他进程的性能。
选择垃圾回收器:根据应用的需求和特性选择合适的垃圾回收器。例如,对于需要低延迟的实时系统,选择并发垃圾回收器(如 CMS 或 G1)可能是个好选择。对于可以容忍更长 GC 停顿时间的批处理任务,使用并行垃圾回收器可能更为合适。
调整新生代和老年代的比例:JVM 的堆内存被划分为新生代和老年代,其中新生代通常分为 Eden 区和两个 Survivor 区(S0和 S1)。这些区域的大小可以通过参数-XX:NewRatio,-XX:SurvivorRatio 进行调整。这种调整可以根据应用的对象生命周期进行,以减少 GC 的次数。
调整线程堆栈大小:使用-Xss 参数可以设置每个线程的堆栈大小。如果应用创建了大量的线程,减小线程堆栈大小可能会帮助减少内存消耗。
启用类数据共享:类数据共享可以加快 JVM 启动速度并减少内存消耗。可以通过-XX:+UseSharedSpaces 参数启用 CDS。
使用 JVM 内建工具进行监控和故障排查:JVM 提供了一些内建工具,如 JConsole, VisualVM, jstat 等,可以用于监控 JVM 的性能和资源使用情况,帮助定位和解决性能问题。
3、什么是JVM垃圾回收?Java中有几种垃圾回收算法?
JVM垃圾回收指的是当Java应用程序运行时,自动释放不再使用的对象内存空间,并进行资源回收和整理过程。JVM提供了几种垃圾回收算法:
- 标记-清除算法:首先标记所有被引用对象,在清除阶段将未标记的对象释放。
- 复制算法:将内存分为相同大小的两块,每次只使用其中的一块。当一块内存用完了之后,将活着的对象复制到另一块内存上,并清除原来使用过的内存。
- 标记-整理算法:先标记所有被引用对象,然后将活着的对象移动到一端,最后清除边界以外的对象。
- 分代收集算法:根据对象生命周期划分为不同代,在垃圾回收时更关注年轻代,因为大多数新生的对象很快就会死去。这种方式减少了全局垃圾检查和处理时间。
4、Java的垃圾回收机制是什么?简要描述它的工作原理。
垃圾回收机制是Java自动管理内存的一种方式,它会自动释放不再被引用或无法访问到的对象所占用的内存。
工作原理如下:
- 标记阶段:从根对象开始遍历所有可达对象,并标记为“活跃”状态。
- 清理阶段:遍历堆内存中所有已分配的对象,将未标记未“活跃”状态的对象清除出堆内存。
- 压缩阶段:在清理完毕后,对堆进行整理,消除由于释放了一些空间而产生的碎片。
四、Java内存管理与性能优化
1、Java的内存区域分为几个部分?
堆(Heap):存放对象实例的地方,包括新生代(Eden区,From Survivor区,To Survivor区)和老年代。
方法区:存储类的结构信息如运行时常量池,字段和方法数据等。
虚拟机栈:存储每个线程的执行信息,包括局部变量、操作数栈、动态链接和方法退出信息。
本地方法栈:为虚拟机使用到的Native方法服务。
程序计数器:指示当前线程正在执行的字节码指令。
2、什么情况下Java新建的对象不存储在Eden中?
- 大对象直接进入老年代:所谓的“大对象”是指需要大量连续内存空间的Java对象。例如,很长的字符串或者大的数组。由于Eden区和Survivor区可能无法存放这样大的对象,所以大对象在新建时直接被分配到老年代中。
- 长期存活的对象进入老年代:Java的垃圾收集器假设大部分对象都是“照生暮死”的,所以采用了分代收集的方法。当Eden区中的对象在Minor GC(小型垃圾收集)后仍然存活,且年龄(被Minor GC清理的次数)达到一定值(默认15)时,这些对象会被移动到老年代中。这个年龄阈值可以通过-XX:MaxTenuringThreshold参数进行设置。
3、Java线程池的核心参数有哪些?
corePoolSize:线程池的基本大小,即在没有任务执行时线程池的大小,并且只有在工作队列满了的情况下,才会创建超出这个数量的线程。
maximunPoolSize:线程池最大线程数。这是线程池可以容纳的最大线程数,超出这个数的线程会被拒绝。
workQueue:当线程池中的线程都被使用时,新任务会被放在这个队列中等待被执行。这个参数通常是一个实现了BlockingQueue、LinkedBlockingQueue或SynchronousQueue。
threadFactory:用于创建新线程的工厂。使用这个工厂可以自定义如何创建线程,例如设置线程的名称、优先级等。
handler:当线程池和工作队列都满了,用于处理被拒绝的任务。常见的实现有AbortPolicy(直接抛出异常)、CallerRunsPolicy(用调用者的线程执行任务)、DiscardPolicy(静默丢弃任务)和DiscardOldestPolicy(丢弃队列中最旧的任务)。
keepAliveTime:当线程池中的线程数量超过corePoolSize时,这是多余的空闲线程在被终止前等待新任务的最长的时间。
timeUnit:keepAliveTime参数的时间单位,如TimeUnit.SECONDS、TimeUnit.MILLISECONDS等。
这些参数共同决定了线程池的创建、运行和任务处理策略。
4、请解释下Java中的线程池是什么,如何使用线程池来提高程序的性能?
Java中的线程池是一种管理和复用线程的机制,可以通过预先创建、管理和重复利用线程对象来优化多线程程序的性能。使用线程池可以减少频繁创建和销毁线程带来的开销,并且可以控制同时执行任务的最大数量。
具体使用方法如下:
- 使用’ExecutorService’接口提供的’newFixedThreadPool’方法初始化一个固定大小的线程池。
- 创建实现了’Runnable’接口的任务。
- 将任务提交给线程池处理,调用’submit()'方法提交任务。
- 线程池会自动分配可用于处理任务的工作线程,并在执行完后将其放回到缓存队列中等待新任务。
优点:使用线程池机制可以避免因频繁创建和销毁大量额外开销,能够高效触发系统资源利用率;同时,还可方便地控制并发协同与资源消耗。
五、Java异常处理和类加载
1、请解释Java中的异常处理机制,包括checked exception和unchecked exception?
Java的异常处理机制适用于处理程序运行时可能出现的错误和异常情况。Java中的异常可分为两种类型:checked exception(被检查的异常)和unchecked exception(未被检查的异常)。
-Checked Exception:这些在编译时由编译器强制要求进行捕获或声明抛出。例如,‘IOException’、'SQLException’等。如果不对这些异常进行try-catch捕获或throws声明抛出,代码将无法通过编译。
-Unchecked Exception:这些是在运行时发生且不需要显式捕获或声明抛出。常见的Unchecked Exceptions包括"NullPointerException",'ArrauIndexOutOfBoundsException’等。
对于Checked Exceptions,开发者必须根据具体情况选择合适的方式来处理它们,比如使用try-catch快来捕获并处理该类异常;而对于Unchecked Exceptions通常可以通过良好的编程实践避免其发生,或者选择适当层次向上抛出以由调用方去负责处理。
2、JVM中一个类加载的过程是什么样子?
- 加载:类加载的第一步是加载,即通过类的全限定名找到对应的二进制字节码文件。这个过程可以通过类加载器完成,类加载器会根据类的名称定位到类文件,并将其读取到内存中。
- 验证:在加载完成后,JVM会对加载的类进行验证,确保类文件的字节码符合JVM规范,不会危害JVM的安全。验证的过程包括文件格式验证、元数据验证、字节码验证以及符号引用验证。
- 准备:在验证通过后,JVM会为类的静态成员遍历分配内存空间,并设置默认初始值。这个过程并不会为实例变量分配空间,只是为静态变量分配。
- 解析:解析阶段是将符号引用替换为直接引用的过程。符号引用是一种编译时的引用,直接引用是在运行时可直接指向内存地址的引用。解析过程包括将常量池中的符号引用替换为直接引用、将类、方法、字段等符号解析为具体的内存地址。
- 初始化:在准备阶段完成后,JVM会开始执行类的初始化过程。类初始化时会执行类的静态代码块和静态变量的赋值操作。这个过程是类加载的最后一个阶段。
- 使用:类加载完成后,就可以使用该类创建对象、调用方法等操作。
六、Java高级特性
1、Java中Synchronized的底层原理是什么?
Synchronized是Java中用于实现线程同步的关键字,它的底层原理是通过对象监视器(也称为内部锁或监视锁)来实现的。
当一个线程进入synchronized代码块时,它会尝试获取对应对象的监视器。如果该监视器没有被其它线程占用,则该线程获取到监视器并执行代码块中的逻辑。如果监视器已经被其它线程占用,该线程就会进入阻塞状态,等待监视器的释放。
在Java虚拟机中,每个对象都有一个与之关联的监视器锁。当一个线程获取到该对象的监视器锁时,其它线程就无法同时获取该对象的监视器锁,它们会被阻塞直到锁被释放。
在方法上适用synchronized关键字时,它会对该方法的整个代码块进行加锁,以保证同一时间只有一个线程可以执行该方法。而在代码块上使用synchronized关键字时,它只会对该代码块进行加锁,其它线程仍然可以同时执行其它非同步代码块。
需要注意的是,synchronized关键字会引入一定的性能开销,因为每次进入synchronized代码块或方法时,都会进行加锁或解锁的操作。因此,在使用synchronized时需要权衡线程安全和性能之间的平衡。
2、泛型?
泛型是Java中一种参数化类型的概念,它允许我们在编写类、接口和方法时使用未知的数据类型。通过使用泛型,我们可以创建通用的代码,可以在不同的数据类型上进行操作,提高代码的复用性和类型安全性。泛型使用尖括号<>来定义,可以用于类、接口和方法的声明和实例化中。在使用泛型时,可以指定具体的数据类型,也可以使用通配符来表示未知的数据类型。
3、常用的反射方法?
常用的反射方法有:
- 获取Class对象:通过Class.forName(),对象.getClass()、类名.class等方式获取一个类的Class对象。
- 创建实例:通过Class对象的newInstance()方法创建类的实例。
- 获取类的成员变量:通过Class对象的getField()、getDeclaredField()等方法获取类的公共或私有成员变量。
- 获取类的方法:通过Class对象的getMethod()、getDeclaredMethod()等方法获取类的公共或私有方法。
- 调用方法:通过Method对象的invoke()方法调用方法。
- 修改成员变量的值:通过Field对象的set()方法修改成员变量的值。
- 调用构造函数:通过Class对象的getConstructor()、getDeclaredConstructor()等方法获取类的公共或私有构造函数,并通过Constructor对象的newInstance()方法创建类的实例。
这些方法可以在运行时动态地获取和操作类的信息,使得我们可以在不知道具体类名地情况下,通过反射机制来调用类地方法、访问成员变量等。
4、Java中深拷贝和浅拷贝的区别是什么?
在Java中,深拷贝和浅拷贝是对于对象复制的两种不同方式。
浅拷贝:是创建一个新对象,该对象的实例变量与原对象相同,如果有引用类型的成员变量,浅拷贝仅仅复制了引用而不是创建新的对象。这意味着原对象和浅拷贝对象会共享相同的引用类型成员变量,对其中一个对象的修改会影响到另一个对象。
深拷贝:是创建一个新对象,该对象的所有实例变量都会被复制,并且会为引用类型的成员变量创建新的对象。这意味着原对象和深拷贝对象是完全独立的,对其中一个对象的修改不会影响到另一个对象。
在Java中,可以通过实现Cloneable接口和重写clone()方法来实现浅拷贝。对于深拷贝,可以通过实现Serializable接口并使用对象序列化和反序列化来实现,或者通过手动复制所有引用类型成员变量的值来实现。
需要注意的是,如果引用类型成员变量也实现了Cloneable接口并进行了深拷贝,那么在进行深拷贝时需要在clone()方法中递归调用成员变量clone()方法,确保所有层级的引用类型对象都被正确复制。