1.Java散列表,树对应的容器类,hashmap如何解决冲突
散列表 Hashmap、hashtable、concurrentHashMap、hashset;
树: treemap、treeset、hashset
treeset 继承自 treemap,hashset 继承自 hashmap ;
性能分析
Map 是 java 中的接口,Map.Entry 是 Map 的一个内部接口 Map 提供了一些常用方法,例如 keySet()、entrySet() 方法等;
Entry: key 和 value的组合,即为一个映射项
Treemap 底层数据结构是红黑树,元素存储是有序的,因此添加元素需要循环查找 Entry 插入位置、取出元素时需要遍历才能找到合适的 Entry ,比较耗性能;treemap、
treeset 对比 hashmap、hashset 优势:前者元素都以有序状态排列
HashMap 产生冲突原因及解决方法
调用hashCode() 计算 hashCode ,两个不同对象可能有相同的 hashCode ,导致冲突产生,
bucket ,哈希表中的数组中可以存储 hashcode 相同对象,每个bucket 都有其指定索引,系统可以根据索引快速访问该 bucket 里存储的元素
HashMap 解决冲突方法
1,开放地址法:通过探测算法,档一个槽位被占用情况下继续查找下一个;
探测算法的三种方式:
线性探查
二次探查
双重散列 采用两个辅助散列函数合成一个:h1 、h2 为两个散列函数
2,链地址法:数组+链表,将hash 值相同对象组织为一个链表放在 hash值对应的 bucket
3,再哈希,准备多个散列函数,当发生冲突时再选择一个散列函数进行散列,原理与双重散列相似
jdk7 与 jdk8 中HashMap的区别
发生冲突
jdk7 中 hashMap 采用数组+链表。如果过多节点在 hash 时发生碰撞,如果要查找其中一个节点,需要 O(n) 的查找时间。
jdk8 中 hashMap 采用数组+链表/红黑树,出现 hash 冲突时会进行判断,该节点是红黑树还是量表:
如果是链表的话,数据插入链表尾部并判断链表长度是否达到某个阈值(默认阈值为 8 ),如果大于阈值,链表将转化为红黑树,时间复杂度为O(nlogn);
若是红黑树的话, 直接插入红黑树即可;
数据结构红黑树的几个性质,查询效率非常高,10亿数据进行不到30次比较就能查找到目标
1、每个节点要么是黑色、要么是红色;
2、根节点是黑色;
3、每个叶子节点是黑色;
4、每个红色节点的两个子结点一定都是黑色;
5、任意一结点到每个叶子节点的路径都包含相同数量的黑节点;
扩容
JDK7 扩容时,在 resize() 过程中采用头插法,旧数据转移到新数组中,转移操作=正序遍历俩表,在头部依次插入,即链表逆序;多线程下 resize() 容易出现 死循环,
在多线程下并发执行 put() 操作,一旦出现扩容情况,容易出现环形链表,在获取数据、遍历链表时出现死循环,即死锁转发太;
JDK 8 在扩容 resize() 时,数据转移时在新链表尾部依次插入,不会出现逆序、环形链表情况,但 jdk 1.8 仍是线程不安全的
使用建议
1,使用出初始值,避免多次扩容的性能消耗;
2,自定义对象作为 key,时需要重写 hashCode 、equals 方法;
3,多线程下, 使用 CurrentHashMap 代替 HashMap;
==============================================================================================================
2.Java实现生产者和消费者的三种方法
方式一:synchronized、wait和notify
定义 Data 资源类,类中定义资源仓库的大小,当前资源个数。资源类的incrment()和decrement()方法是synchronized 的。生产者/消费者线程共享一个资源Data。
方式二:lock和condition的await、signalAll
方式三:BlockingQueue
定义Data资源类,资源类持有一个BlockingQueue。生产者/消费者线程共享一个资源类Data的成员变量,调用Queue的put()和take() 实现生产和消费。
==============================================================================================================
在设计实现该模式的时候,需要注意以下三个问题:
对容器取产品和添加产品时的同步问题,即任意时刻,取与添加是互斥的,且每一次取操作之间也是互斥的(添加操作也是如此)。
方法1:
使用wait()和notify(),当容器满了之后,生产者需要暂时停止生产,直到容器非满,当容器空了之后,消费者需要暂时停止消费,直到容器非空,缓冲区为满和为空时都调用wait()方法等待,
直到生产者生产了一个产品或者消费者消费了一个产品之后才会唤醒所有线程。
方法2:
await()与signal()方法,即线程锁的方式。
可重入锁(ReentrantLock)的实现。
可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
简单来说,该锁维护这一个与获取锁相关的计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,函数调用结束计数器就减1,然后锁需要被释放两次才能获得真正释放。
已经获取锁的线程进入其他需要相同锁的同步代码块不会被阻塞。
方法3:
阻塞队列(BlockingQueue)的实现。
被阻塞的情况主要有如下两种:
1.当队列满了的时候进行入队列操作
2.当队列空了的时候进行出队列操作
当一个线程对已经满了的阻塞队列进行入队操作时会阻塞,除非有另外一个线程进行了出队操作,当一个线程对一个空的阻塞队列进行出队操作时也会阻塞,除非有另外一个线程进行了
入队操作。 所以,阻塞队列是线程安全的。
=========================================================================================================
3.init方法与clinit方法的区别
一 clinit初始化/静态方法
类型初始化方法主要是对static变量进行初始化操作,对static域和static代码块初始化的逻辑全部封装在方法中。
java.lang.Class.forName(String name, boolean initialize,ClassLoader loader),其中第二个参数就是是否需要初始化。
Java类型初始化过程中对static变量的初始化操作依赖于static域和static代码块的前后关系,static域与static代码块声明的位置关系会导致java编译器生成方法字节码。
类型的初始化方法只在该类型被加载时才执行,且只执行一次。
二 对象实例化方法
Java对象在被创建时,会进行实例化操作。该部分操作封装在方法中,并且子类的方法中会首先对父类方法的调用。
Java对象实例化过程中对实例域的初始化赋值操作全部在方法中进行,方法显式的调用父类的方法,实例域的声明以及实例初始化语句块同样的位置关系会影响编译器生成的方
法的字节码顺序,方法以构造方法作为结束。
三 init和clinit区别:
①init和clinit方法执行时机不同
init是对象构造器方法,也就是说在程序执行 new 一个对象调用该对象类的 constructor 方法时才会执行init方法,而clinit是类构造器方法,也就是在jvm进行类加载—–验证
—-解析—–初始化,中的初始化阶段jvm会调用clinit方法。
②init和clinit方法执行目的不同
=================================================================================================
1.init与clinit方法执行时机不同
init是对象构造器方法,在程序执行new 一个对象类的constructor方法时才会执行init方法。
clinit是类构造器方法,在JVM进行类加载–验证–解析–初始化中的初始化阶段才会调用clinit方法。
2.init和clinit方法执行目的不同
init是 (instance) 实例构造器,对非静态变量解析和初始化。
而clinit是 (class) 类构造器对静态变量、静态代码块进行初始化。
=========================================================================================================
4.Java中的引用
Java中有四种类型的引用: 强引用、软引用、弱引用和虚引用。
1.强引用:
强引用也称为普通引用,是最常见的一种引用形式是使用最广泛的一种引用。通过使用强引用,可以直接访问对象,Java 会尽可能地确保它永远不会被垃圾回收。
如果一个对象具有强3用,那么它将一直存在,直到程序明确地将其设置为 nul1,才会被垃回收机制回收。
2.软引用(SoftReference):
软引用是一种比强引用更弱的引用,可以帮助我们在内存不足的情况下来释放对象占用的内存空间,即使内存空间充足,也会延迟垃圾回收。
3.弱引用(WeakReference):
弱引用是比软引用更弱的引用,可以在系统内存不足时帮助我们回收对象所占用的内存空间,而且不管内存是否充足,只要发生垃圾回收,它都会将弱引用指向的对象回收。
4.虚引用(PhantomReference):
虚引用也称为幽灵引用或者空引用,它是最弱的一种引用,无法通过虚引用来获取一个对象实例,当一个对象只具有虚引用时,它就和没有任何引用一样,
在任何时候都可能被垃圾回收机制回收。虚引用主要用于跟踪对象被垃圾收集器回收的活动。
==========================================================================================================
5.Java对象的创建过程
(1)类加载检查
Java虚拟机(jvm)在读取一条new指令时候,首先检查能否在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否被加载、解析和初始化。如果没有,
则会先执行相应的类加载过程。
(2)内存分配
在通过(1)后,则开始为新生的对象分配内存。该对象所需的内存大小在类加载完成后便可确定,因此为每个对象分配的内存大小是确定的。而分配方式主要有两种,
分别为:
1.指针碰撞
应用场合:堆内存规整(通俗的说就是用过的内存被整齐充分的利用,用过的内存放在一边,没有用过的放在另外一边,而中间利用一个分界值指针对这两边的内存进行分界,
从而掌握内存分配情况)。即在开辟内存空间时候,将分界值指针往没用过的内存方向移动向应大小位置即可)。
将堆内存这样划分的代表的GC收集器算法有:Serial,ParNew
2.空闲列表
应用场合;堆内存不规整(虚拟机维护一个可以记录内存块是否可以用的列表来了解内存分配情况)即在开辟内存空间时候,找到一块足够大的内存块分配给该对象即可,
同时更新记录列表。
将堆内存这样划分的代表的GC收集器算法有:CMS
(3)初始化默认值
第(2)步完成后,紧接着,虚拟机需要将分配到的内存空间都进行初始化(即给一些默认值),这将做是为了保证对象实例的字段在Java代码中可以在不赋初值的情况下使用。
程序可以访问到这些字段对用数据类型的默认值。
(4)设置对象头
初始化(3)完成后,虚拟机对对象进行一些简单设置,如标记该对象是哪个类的实例,这个对象的hash码,该对象所处的年龄段等等(这些可以理解为对象实例的基本信息)。
这些信息被写在对象头中。jvm根据当前的运行状态,会给出不同的设置方式。
(5)执行初始化方法
在(4)完成后,最后执行由开发人员编写的对象的初始化方法,把对象按照开发人员的设计进行初始化,一个对象便创建出来了。
===========================================================================================================
6.Java中创建子类实例时会创建父类实例?
不会
首先每个类的这些元数据,无论是在构建这个类的实例还是调用这个类某个对象的方法,都会访问方法区的这些元数据。构建一个对象时,JVM会在堆中给对象分配空间,
这些空间用来存储当前对象实例属性以及其父类的实例属性(而这些属性信息都是从方法区获得)
注意,这里并不是仅仅为当前对象的实例属性分配空间,还需要给父类的实例属性分配。
总之,会为父类分配堆内存,但是这块内存属于子类的堆内存。
============================================================================================================
7.Java的类加载机制 为什么会出现锁机制?
java虚拟机将编译后的class文件加载到内存中,进行校验、转换、解析和初始化,到最终的使用。这就是java类加载机制;
Java 允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据出现不正确的结果,相互之间产生冲突,因此加入锁保证了
该变量的唯一性和准确性。
============================================================================================================
8.抽象类和接口的区别
含有abstract修饰符的class即为抽象类,abstract 类不能创建实例对象。含有abstract方法的类必须定义为abstract class,abstract class类中的方法不必是抽象的。
abstract class类中定义抽象方法必须在具体(Concrete)子类中实现,所以,不能有抽象构造方法或抽象静态方法。如果子类没有实现抽象父类中的所有抽象方法,
那么子类也必须定义为abstract类型。
接口(interface)可以说成是抽象类的一种特例,接口中的所有方法都必须是抽象的。接口中的方法定义默认为public abstract类型,接口中的成员变量类型默认为public static final。
1.抽象类可以有构造方法,接口中不能有构造方法。
2.抽象类中可以有普通成员变量,接口中没有普通成员变量
3.抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法。
4. 抽象类中的抽象方法的访问类型可以是public,protected和(默认类型,虽然eclipse下不报错,但应该也不行),但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型。
5. 抽象类中可以包含静态方法,接口中不能包含静态方法
6. 抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。
7. 一个类可以实现多个接口,但只能继承一个抽象类。
=======================================================================================================================
9.双亲委派模型:启动加载器、扩展加载器、应用程序加载器
Java中的类和类加载器一起 唯一确定类在jvm中的一致性。
BootStrapClassLoader <- ExtClassLoader <- AppClassLoader <- 自定义类加载器
启动类加载器:主要负责加载 java.home 下的 lib 目录下的类库或者被 -Xbootclasspath 参数指定的路径中的类库。应用程序不能直接使用该加载器。
扩展类加载器:负责加载 java.home 下的 lib/ext 下的类或者系统变量 java.ext.dirs 指定路径下的类库。开发者可以使用该加载器。
应用程序类加载器:负责加载用户指定的路径 即 classPath 下的类库。如果程序员没有自定义类加载器,默认调用该加载器。
双亲委派模型要求,除了顶层的启动类加载器以外,其他的类加载器都要有自己的父类加载器,使用组合关系复用父类加载器。
过程:当用户需要加载器时,首先不会自己去加载这个类,而是将请求委托给父类加载器,每一层都是如此,因此最终都会到达顶层的类加载器。只有当父类加载器反馈不能加载,
才会将加载的任务给子类加载器。
好处:让Java类同其类加载器一起具备了一种带优先级。比如我们要加载java.lang.Object类,无论我们用哪个类加载器去加载Object类,这个加载请求最终都会委托给Bootstrap ClassLoader,
这样就保证了所有加载器加载的Object类都是同一个类。这样保证了Java程序运行的稳定性。
============================================================================================================
10.重载与重写
重载的概念 :
在同一个类中,允许存在一个以上的同名方法,只要它们的参数个数或者参数 类型不同即可。
重写的概念:
在子类中可以根据需要对从父类中继承来的方法进行改造,也称 为方法的重置、覆盖。在程序执行时,子类的方法将覆盖父类的方法。
重载和重写的特点:
重载的特点:
与返回值类型无关,只看参数列表,且参数列表必须不同。(参数个数或参数类 型)。调用时,根据方法参数列表的不同来区别。
重写的特点:
1.子类重写的方法必须和父类被重写的方法具有相同的方法名称、参数列表 。
2.子类重写的方法的返回值类型不能大于父类被重写的方法的返回值类型 (返回类型为类)。
3.子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限 。
4.子类不能重写父类中声明为private权限的方法 。
5.子类方法抛出的异常不能大于父类被重写方法的异常 (父类异常的子类)。
============================================================================================================
Java面试题2
最新推荐文章于 2024-07-13 22:56:10 发布