java面试题及答案

何时会触发full gc

  1. System.gc()方法的调用java

  2. 老年代空间不足程序员

  3. 永生区空间不足(JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息、常量、静态变量等数据)算法

  4. GC时出现promotion failed和concurrent mode failure数据库

  5. 统计获得的Minor GC晋升到旧生代平均大小大于老年代剩余空间bootstrap

  6. 堆中分配很大的对象数组

能够做为root的对象:

  1. 类中的静态变量,当它持有一个指向一个对象的引用时,它就做为root浏览器

  2. 活动着的线程,能够做为root缓存

  3. 一个Java方法的参数或者该方法中的局部变量,这两种对象能够做为root安全

  4. JNI方法中的局部变量或者参数,这两种对象能够做为root服务器

例子:下述的Something和Apple均可以做为root对象。

public AClass{

  public static Something;
  public static final Apple;
   ''''''
}

 

Java方法的参数和方法中的局部变量,能够做为root.

public Aclass{

public void doSomething(Object A){
    ObjectB b = new ObjectB; 
    }
 }

 

新生代转移到老年代的触发条件

  1. 长期存活的对象

  2. 大对象直接进入老年代

  3. minor gc后,survivor仍然放不下

  4. 动态年龄判断 ,大于等于某个年龄的对象超过了survivor空间一半 ,大于等于某个年龄的对象直接进入老年代

G1和CMS的区别

  1. G1同时回收老年代和年轻代,而CMS只能回收老年代,须要配合一个年轻代收集器。另外G1的分代更可能是逻辑上的概念,G1将内存分红多个等大小的region,Eden/ Survivor/Old分别是一部分region的逻辑集合,物理上内存地址并不连续。

    watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=
  2. CMS在old gc的时候会回收整个Old区,对G1来讲没有old gc的概念,而是区分Fully young gc和Mixed gc,前者对应年轻代的垃圾回收,后者混合了年轻代和部分老年代的收集,所以每次收集确定会回收年轻代,老年代根据内存状况能够不回收或者回收部分或者所有(这种状况应该是可能出现)。

 

双亲委派模型中有哪些方法。用户如何自定义类加载器 。怎么打破双亲委托机制

  1. 双亲委派模型中用到的方法:

  • findLoadedClass(),

  • loadClass()

  • findBootstrapClassOrNull()

  • findClass()

  • defineClass():把二进制数据转换成字节码。

  • resolveClass()

自定义类加载器的方法:继承 ClassLoader 类,重写 findClass()方法 。

  1. 继承ClassLoader覆盖loadClass方法 原顺序

  2. findLoadedClass

  3. 委托parent加载器加载(这里注意bootstrap加载器的parent为null)

  4. 自行加载 打破委派机制要作的就是打乱2和3的顺序,经过类名筛选本身要加载的类,其余的委托给parent加载器。

即时编译器的优化方法

字节码能够经过如下两种方式转换成合适的语言:

  1. 解释器

  2. 即时编译器 即时编译器把整段字节码编译成本地代码,执行本地代码比一条一条进行解释执行的速度快不少,由于本地代码是保存在缓存里的

编译过程的五个阶段

  1. 第一阶段:词法分析

  2. 第二阶段:语法分析

  3. 第三阶段:词义分析与中间代码产生

  4. 第四阶段:优化

  5. 第五阶段:目标代码生成

java应用系统运行速度慢的解决方法

问题解决思路:

  1. 查看部署应用系统的系统资源使用状况,CPU,内存,IO这几个方面去看。找到对就的进程。

  2. 使用jstack,jmap等命令查看是JVM是在在什么类型的内存空间中作GC(内存回收),和查看GC日志查看是那段代码在占用内存。首先,调节内存的参数设置,若是仍是同样的问题,就要定位到相应的代码。

  3. 定位代码,修改代码(通常是代码的逻辑问题,或者代码获取的数据量过大。)

 

内存溢出是什么,什么缘由致使的

内存溢出是指应用系统中存在没法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能提供的最大内存。为了解决Java中内存溢出问题,咱们首先必须了解Java是如何管理内存的。Java的内存管理就是对象的分配和释放问题。在Java中,内存的分配是由程序完成的,而内存的释放是由垃圾收集器(Garbage Collection,GC)完成的,程序员不须要经过调用GC函数来释放内存,由于不一样的JVM实现者可能使用不一样的算法管理GC,有的是内存使用到达必定程度时,GC才开始工做,也有定时执行的,有的是中断式执行GC。但GC只能回收无用而且再也不被其它对象引用的那些对象所占用的空间。Java的内存垃圾回收机制是从程序的主要运行对象开始检查引用链,当遍历一遍后发现没有被引用的孤立对象就做为垃圾回收。

 

引发内存溢出的缘由有不少种,常见的有如下几种:

  1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;

  2. 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;

  3. 代码中存在死循环或循环产生过多重复的对象实体;

  4. 使用的第三方软件中的BUG;

  5. 启动参数内存值设定的太小;

 

内存溢出的解决

内存溢出虽然很棘手,但也有相应的解决办法,能够按照从易到难,一步步的解决。

 

第一步,就是修改JVM启动参数,直接增长内存。这一点看上去彷佛很简单,但很容易被忽略。JVM默承认以使用的内存为64M,Tomcat默承认以使用的内存为128MB,对于稍复杂一点的系统就会不够用。在某项目中,就由于启动参数使用的默认值,常常报“OutOfMemory”错误。所以,-Xms,-Xmx参数必定不要忘记加。

 

第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。在一个项目中,使用两个数据库链接,其中专用于发送短信的数据库链接使用DBCP链接池管理,用户为不将短信发出,有意将数据库链接用户名改错,使得日志中有许多数据库链接异常的日志,一段时间后,就出现“OutOfMemory”错误。经分析,这是因为DBCP链接池BUG引发的,数据库链接不上后,没有将链接释放,最终使得DBCP报“OutOfMemory”错误。通过修改正确数据库链接参数后,就没有再出现内存溢出的错误。

 

查看日志对于分析内存溢出是很是重要的,经过仔细查看日志,分析内存溢出前作过哪些操做,能够大体定位有问题的模块。

 

第三步,找出可能发生内存溢出的位置。重点排查如下几点:

  1. 检查代码中是否有死循环或递归调用。

  2. 检查是否有大循环重复产生新对象实体。

  3. 检查对数据库查询中,是否有一次得到所有数据的查询。通常来讲,若是一次取十万条记录到内存,就可能引发内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引发内存溢出。所以对于数据库查询尽可能采用分页的方式查询。

  4. 检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。

 

第四步,使用内存查看工具动态查看内存使用状况。某个项目上线后,每次系统启动两天后,就会出现内存溢出的错误。这种状况通常是代码中出现了缓慢的内存泄漏,用上面三个步骤解决不了,这就须要使用内存查看工具了。

 

内存查看工具备许多,比较有名的有:Optimizeit Profiler、JProbe Profiler、JinSight和Java1.5的Jconsole等。它们的基本工做原理大同小异,都是监测Java程序运行时全部对象的申请、释放等动做,将内存管理的全部信息进行统计、分析、可视化。开发人员能够根据这些信息判断程序是否有内存泄漏问题。通常来讲,一个正常的系统在其启动完成后其内存的占用量是基本稳定的,而不该该是无限制的增加的。持续地观察系统运行时使用的内存的大小,能够看到在内存使用监控窗口中是基本规则的锯齿形的图线,若是内存的大小持续地增加,则说明系统存在内存泄漏问题。经过间隔一段时间取一次内存快照,而后对内存快照中对象的使用与引用等信息进行比对与分析,能够找出是哪一个类的对象在泄漏。

 

经过以上四个步骤的分析与处理,基本能处理内存溢出的问题。固然,在这些过程当中也须要至关的经验与敏感度,须要在实际的开发与调试过程当中不断积累。

 

整体上来讲,产生内存溢出是因为代码写的很差形成的,所以提升代码的质量是最根本的解决办法。有的人认为先把功能实现,有BUG时再在测试阶段进行修正,这种想法是错误的。正如一件产品的质量是在生产制造的过程当中决定的,而不是质量检测时决定的,软件的质量在设计与编码阶段就已经决定了,测试只是对软件质量的一个验证,由于测试不可能找出软件中全部的BUG。

 

JAVA 线程状态转换图示


watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

synchronized 的底层怎么实现

  1. 同步代码块(Synchronization)基于进入和退出管程(Monitor)对象实现。每一个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的全部权,过程以下:

  • 若是monitor的进入数为0,则该线程进入monitor,而后将进入数设置为1,该线程即为monitor的全部者。

  • 若是线程已经占有该monitor,只是从新进入,则进入monitor的进入数加1.

  • 若是其余线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再从新尝试获取monitor的全部权。

  1. 被 synchronized 修饰的同步方法并无经过指令monitorenter和monitorexit来完成(理论上其实也能够经过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,若是设置了,执行线程将先获取monitor,获取成功以后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其余任何线程都没法再得到同一个monitor对象。其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需经过字节码来完成

讲一下CAS

CAS,compare and swap的缩写,中文翻译成比较并交换。乐观锁用到的机制就是CAS,每次不加锁而是假设没有冲突而去完成某项操做,若是由于冲突失败就重试。

 

原理:

  1. CAS有3个操做数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改成B,不然什么都不作。

JDK文档说cas同时具备volatile读和volatile写的内存语义。

 

缺点:

  1. ABA问题。由于CAS须要在操做值的时候检查下值有没有发生变化,若是没有发生变化则更新,可是若是一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化

  2. 循环时间长开销大。自旋CAS若是长时间不成功,会给CPU带来很是大的执行开销。

  3. 只能保证一个共享变量的原子操做。对多个共享变量操做时,循环CAS就没法保证操做的原子性,这个时候就能够用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操做。好比有两个共享变量i=2,j=a,合并一下ij=2a,而后用CAS来操做ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你能够把多个变量放在一个对象里来进行CAS操做。

线程池

Executor线程池框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。

 

ThreadPoolExecutor执行的策略

  1. 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务

  2. 线程数量达到了corePools,则将任务移入队列等待

  3. 队列已满,新建线程(非核心线程)执行任务

  4. 队列已满,总线程数又达到了maximumPoolSize,就会由(RejectedExecutionHandler)抛出异常

新建线程 -> 达到核心数 -> 加入队列 -> 新建线程(非核心) -> 达到最大数 -> 触发拒绝策略

 

常见四种线程池

  1. CachedThreadPool():可缓存线程池。

  • 线程数无限制

  • 有空闲线程则复用空闲线程,若无空闲线程则新建线程

  • 必定程序减小频繁建立/销毁线程,减小系统开销

  1. FixedThreadPool():定长线程池。

  • 可控制线程最大并发数(同时执行的线程数)

  • 超出的线程会在队列中等待

  1. ScheduledThreadPool():定时线程池。

  • 支持定时及周期性任务执行。

  1. SingleThreadExecutor():单线程化的线程池。

  • 有且仅有一个工做线程执行任务

  • 全部任务按照指定顺序执行,即遵循队列的入队出队规则

 

 

 

 

四种拒绝策略

  1. AbortPolicy:拒绝任务,且还抛出RejectedExecutionException异常,线程池默认策略

  2. CallerRunPolicy:拒绝新任务进入,若是该线程池尚未被关闭,那么这个新的任务在执行线程中被调用

  3. DiscardOldestPolicy: 若是执行程序还没有关闭,则位于头部的任务将会被移除,而后重试执行任务(再次失败,则重复该过程),这样将会致使新的任务将会被执行,而先前的任务将会被移除。

  4. DiscardPolicy:没有添加进去的任务将会被抛弃,也不抛出异常。基本上为静默模式。

 

为何要用线程池

  1. 减小了建立和销毁线程的次数,每一个工做线程均可以被重复利用,可执行多个任务。

  2. 运用线程池能有效的控制线程最大并发数,能够根据系统的承受能力,调整线程池中工做线线程的数目,防止由于消耗过多的内存,而把服务器累趴下(每一个线程须要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

  3. 对线程进行一些简单的管理,好比:延时执行、定时循环执行的策略等,运用线程池都能进行很好的实现

对象锁和静态锁之间的区别
  1. 对象锁用于对象实例方法,

  2. 类锁用于类的静态方法或一个类的class对象。

  3. 类的对象实例能够有不少,不一样对象实例的对象锁互不干扰,而每一个类只有一个类锁

 

简述volatile字

两个特性

  1. 保证了不一样线程对这个变量进行 读取 时的可见性,即一个线程修改 了某个变量的值 , 这新值对其余线程来讲是当即可见的 。(volatile 解决了 线程间 共享变量

  2. 禁止进行指令重排序 ,阻止编译器对代码的优化

要想并发程序正确地执行,必需要保证原子性、可见性以及有序性,锁保证了原子性,而volatile保证可见性和有序性

happens-before 原则(先行发生原则):

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操做先行发生于书写在 后面的操做

  2. 锁定规则:一个 unLock 操做先行发生于后面对同一个锁的 lock 操做

  3. volatile 变量规则:对一个变量的写操做先行发生于后面对这个变量的读操做

  4. 传递规则:若是操做 A 先行发生于操做 B,而操做 B 又先行发生于操做 C,则能够 得出操做 A 先行发生于操做 C

  5. 线程启动规则:Thread 对象的 start()方法先行发生于此线程的每一个一个动做

  6. 线程中断规则:对线程 interrupt()方法的调用先行发生于被中断线程的代码检测 到中断事件的发生

  7. 线程终结规则:线程中全部的操做都先行发生于线程的终止检测,咱们能够经过 T hread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行

  8. 对象终结规则:一个对象的初始化完成先行发生于他的 finalize()方法的开始

Lock 和synchronized 的区别

  1. Lock 是一个 接口,而 synchronized 是 Java 中的 关键字, synchronized 是 内置的语言实现;

  2. synchronized 在 发生异常时,会 自动释放线程占有的锁,所以 不会导 致死锁现象发生;而 Lock 在发生异常时,若是没有主动经过 unLock()去释放 锁,则很 可能形成死锁现象,所以用 使用 Lock 时须要在 finally 块中释放锁;

  3. Lock 可让 等待锁的线程响应中断 (可中断锁),而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去, 不可以响应中 断 (不可中断锁);

  4. 经过 Lock 能够知道 有没有成功获取锁 (tryLock ( ) 方法 :若是获取 了锁 ,回 则返回 true ;回 不然返回 false e, , 也就说这个方法不管如何都会当即返回 。在拿不到锁时不会一直在那等待。),而 synchronized 却没法办到。

  5. Lock 能够提升 多个线程进行读操做的效率( 读写锁)。

  6. Lock 能够实现 公平锁,synchronized 不保证公平性。在性能上来讲,若是线程竞争资源不激烈时,二者的性能是差很少的,而 当竞争资源很是激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优 于 synchronized。因此说,在具体使用时要根据适当状况选择。

 

ThreadLocal(线程变量副本)

Synchronized实现内存共享,ThreadLocal为每一个线程维护一个本地变量。采用空间换时间,它用于线程间的数据隔离,为每个使用该变量的线程提供一个副本,每一个线程均可以独立地改变本身的副本,而不会和其余线程的副本冲突。ThreadLocal类中维护一个Map,用于存储每个线程的变量副本,Map中元素的键为线程对象,而值为对应线程的变量副本。ThreadLocal在Spring中发挥着巨大的做用,在管理Request做用域中的Bean、事务管理、任务调度、AOP等模块都出现了它的身影。Spring中绝大部分Bean均可以声明成Singleton做用域,采用ThreadLocal进行封装,所以有状态的Bean就可以以singleton的方式在多线程中正常工做了。

经过Callable和Future建立线程

Java 5在concurrency包中引入了java.util.concurrent.Callable 接口,它和Runnable接口很类似,但它能够返回一个对象或者抛出一个异常。

 

Callable接口使用泛型去定义它的返回类型。Executors类提供了一些有用的方法去在线程池中执行Callable内的任务。因为Callable任务是并行的,咱们必须等待它返回的结果。java.util.concurrent.Future对象为咱们解决了这个问题。在线程池提交Callable任务后返回了一个Future对象,使用它咱们能够知道Callable任务的状态和获得Callable返回的执行结果。Future提供了get()方法让咱们能够等待Callable结束并获取它的执行结果。

 

  1. 建立Callable接口的实现类,并实现call()方法,该call()方法将做为线程执行体,而且有返回值。

  2. 建立Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

  3. 使用FutureTask对象做为Thread对象的target建立并启动新线程。

  4. 调用FutureTask对象的get()方法来得到子线程执行结束后的返回值

 

什么叫守护线程,用什么方法实现守护线程(Thread.setDeamon()的含义)

在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) 用个比较通俗的好比,任何一个守护线程都是整个JVM中全部非守护线程的保姆:只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工做。JVM内部的实现是若是运行的程序只剩下守护线程的话,程序将终止运行,直接结束。因此守护线程是做为辅助线程存在的,主要的做用是提供计数等等辅助的功能。

如何中止一个线程?

终止线程的三种方法:

  1. 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。在定义退出标志exit时,使用了一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值,

  thread.exit = true;  // 终止线程thread 
 
  1. 使用stop方法强行终止线程(这个方法不推荐使用,由于stop和suspend、resume同样,也可能发生不可预料的结果)。使用stop方法能够强行终止正在运行或挂起的线程。咱们可使用以下的代码来终止线程:thread.stop(); 虽然使用上面的代码能够终止线程,但使用stop方法是很危险的,就象忽然关闭计算机电源,而不是按正常程序关机同样,可能会产生不可预料的结果,所以,并不推荐使用stop方法来终止线程。

  2. 使用interrupt方法中断线程,使用interrupt方法来终端线程可分为两种状况:

  • 线程处于阻塞状态,如使用了sleep方法。

  • 使用while(!isInterrupted()){……}来判断线程是否被中断。在第一种状况下使用interrupt方法,sleep方法将抛出一个InterruptedException例外,而在第二种状况下线程将直接退出。

注意:在Thread类中有两个方法能够判断线程是否经过interrupt方法被终止。一个是静态的方法interrupted(),一个是非静态的方法isInterrupted(),这两个方法的区别是interrupted用来判断当前线是否被中断,而isInterrupted能够用来判断其余线程是否被中断。所以,while (!isInterrupted())也能够换成while (!Thread.interrupted())。

什么是线程安全?什么是线程不安全?

  1. 线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其余线程不能进行访问直到该线程读取完,其余线程才可以使用。不会出现数据不一致或者数据污染。

  2. 线程不安全就是不提供数据访问保护,有可能出现多个线程前后更改数据形成所获得的数据是脏数据 在多线程的状况下,因为同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问。

 

 

HashSet和TreeSet区别

HashSet

  1. 不能保证元素的排列顺序,顺序有可能发生变化

  2. 不是同步的

  3. 集合元素能够是null,但只能放入一个null 当向HashSet结合中存入一个元素时,HashSet会调用该对象的hashCode()方法来获得该对象的hashCode值,而后根据 hashCode值来决定该对象在HashSet中存储位置。

TreeSet

  1. TreeSet是SortedSet接口的惟一实现类

  2. TreeSet能够确保集合元素处于排序状态。TreeSet支持两种排序方式,天然排序 和定制排序,其中天然排序为默认的排序方式。向TreeSet中加入的应该是同一个类的对象

讲一下LinkedHashMap

LinkedHashMap的实现就是HashMap+LinkedList的实现方式,以HashMap维护数据结构,以LinkList的方式维护数据插入顺序

 

LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先获得的记录确定是先插入的。在遍历的时候会比HashMap慢TreeMap可以把它保存的记录根据键排序,默认是按升序排序,也能够指定排序的比较器

 

利用LinkedHashMap实现LRU算法缓存(

  1. LinkedList首先它是一个Map,Map是基于K-V的,和缓存一致

  2. LinkedList提供了一个boolean值可让用户指定是否实现LRU)

 

Java8 中HashMap的优化(引入红黑树的数据结构和扩容的优化)

  1. if (binCount >= TREEIFY_THRESHOLD - 1) 当符合这个条件的时候,把链表变成treemap红黑树,这样查找效率从o(n)变成了o(log n) ,在JDK1.8的实现中,优化了高位运算的算法,经过hashCode()的高16位异或低16位实现的:

  2. 咱们使用的是2次幂的扩展(指长度扩为原来2倍),因此,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置

 

这里的Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算。

元素在从新计算hash以后,由于n变为2倍,那么n-1的mask范围在高位多1bit(红色),所以新的index就会发生这样的变化:hashMap 1.8 哈希算法例图2

 


watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

所以,咱们在扩充HashMap的时候,不须要像JDK1.7的实现那样从新计算hash,只须要看看原来的hash值新增的那个bit是1仍是0就行了,是0的话索引没变,是1的话索引变成“原索引+oldCap”

Map遍历的keySet()和entrySet()性能差别缘由

Set<Entry<StringString>> entrySet = map.entrySet();
Set<Stringset = map.keySet();`
  1. keySet()循环中经过key获取对应的value的时候又会调用getEntry()进行循环。循环两次

  2. entrySet()直接使用getEntry()方法获取结果,循环一次

  3. 因此 keySet()的性能会比entrySet()差点。因此遍历map的话仍是用entrySet()来遍历

public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }  

 

 

final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
}

 

抽象类和接口的对比

 

 

参数 抽象类 接口
默认的方法实现 它能够有默认的方法实现 接口彻底是抽象的。它根本不存在方法的实现
实现 子类使用extends关键字来继承抽象类。若是子类不是抽象类的话,它须要提供抽象类中全部声明的方法的实现。 子类使用关键字implements来实现接口。它须要提供接口中全部声明的方法的实现
构造器 抽象类能够有构造器 接口不能有构造器
与正常Java类的区别 除了你不能实例化抽象类以外,它和普通Java类没有任何区别 接口是彻底不一样的类型
访问修饰符 抽象方法能够有public、protected和default这些修饰符 接口方法默认修饰符是public。你不可使用其它修饰符。
main方法 抽象方法能够有main方法而且咱们能够运行它 接口没有main方法,所以咱们不能运行它。
多继承 抽象方法能够继承一个类和实现多个接口 接口只能够继承一个或多个其它接口
速度 它比接口速度要快 接口是稍微有点慢的,由于它须要时间去寻找在类中实现的方法。
添加新方法 若是你往抽象类中添加新的方法,你能够给它提供默认的实现。所以你不须要改变你如今的代码。 若是你往接口中添加方法,那么你必须改变实现该接口的类。

 

建立一个类的几种方法?

  1. 使用new关键字 → 调用了构造函数

  2. 使用Class类的newInstance方法  → 调用了构造函数

 

Employee emp2 = (Employee)Class.forName("org.programming.mitra.exercises.Employee").newInstance();

 

  1. 使用Constructor类的newInstance方法  → 调用了构造函数

Constructor<Employee> constructor = Employee.class.getConstructor();
Employee emp3 = constructor.newInstance();
 
  1. 使用clone方法   → 没有调用构造函数

  2. 使用反序列化 }→ 没有调用构造函数

ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.obj"));
Employee emp5 = (Employee) in.readObject();

Redirect和forward

  1. 上图所示的间接转发请求的过程以下:浏览器向Servlet1发出访问请求;Servlet1调用sendRedirect()方法,将浏览器重定向到Servlet2;浏览器向servlet2发出请求;最终由Servlet2作出响应。

  2. 上图所示的直接转发请求的过程以下:浏览器向Servlet1发出访问请求;Servlet1调用forward()方法,在服务器端将请求转发给Servlet2;最终由Servlet2作出响应。

 

什么是泛型,为何要使用以及类型擦除

  1. 泛型的本质就是“参数化类型”,也就是说所操做的数据类型被指定为一个参数。建立集合时就指定集合元素的数据类型,该集合只能保存其指定类型的元素, 避免使用强制类型转换。

  2. Java 编译器生成的字节码是不包含泛型信息的,泛型类型信息将在 编译处理 时 被擦除,这个过程即 类型擦除。类型擦除能够简单的理解为将泛型 java 代码转 换为普通 java 代码,只不过编译器更直接点,将泛型 java 代码直接转换成普通 java 字节码。

类型擦除的主要过程以下:

  1. 将全部的泛型参数用其最左边界(最顶级的父类型)类型替换。

  2. 移除全部的类型参数。

Object跟这些标记符表明的java类型有啥区别呢?

Object是全部类的根类,任何类的对象均可以设置给该Object引用变量,使用的时候可能须要类型强制转换,可是用使用了泛型T、E等这些标识符后,在实际用以前类型就已经肯定了,不须要再进行类型强制转换。

Error类和Exception类区别

  1. Error类和Exception类的父类都是throwable类,他们的区别是:Error类通常是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。对于这类错误的致使的应用程序中断,仅靠程序自己没法恢复和和预防,遇到这样的错误,建议让程序终止。Exception类表示程序能够处理的异常,能够捕获且可能恢复。遇到这类异常,应该尽量处理异常,使程序恢复运行, 而不该该随意终止异常。

  2. Exception类又分为运行时异常(Runtime Exception)和受检查的异常(Checked Exception ),运行时异常;ArithmaticException,IllegalArgumentException,编译能经过,可是一运行就终止了,程序不会处理运行时异常,出现这类异常,程序会终止。而受检查的异常,要么用try。。。catch捕获,要么用throws字句声明抛出,交给它的父类处理,不然编译不会经过。

 

throw和throws区别

throw:(针对对象的作法)抛出一个异常,能够是系统定义的,也能够是本身定义的

public void yichang(){
    NumberFormatException e = new NumberFormatException();
    throw e;
}

 

throws:(针对一个方法抛出的异常)抛出一个异常,能够是系统定义的,也能够是本身定义的。

public void yichang() throws NumberFormatException{
    int a = Integer.parseInt("10L");
}

 

  1. throws出如今方法函数头;而throw出如今函数体。

  2. throws表示出现异常的一种可能性,并不必定会发生这些异常;throw则是抛出了异常,执行throw则必定抛出了某种异常。

  3. 二者都是消极处理异常的方式(这里的消极并非说这种方式很差),只是抛出或者可能抛出异常,可是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。

 

.class 文件是什么类型文件

class文件是一种8位字节的二进制流文件

java中序列化之子类继承父类序列化

父类实现了Serializable,子类不须要实现Serializable

 

相关注意事项 1. 序列化时,只对对象的状态进行保存,而无论对象的方法;2. 当一个父类实现序列化,子类自动实现序列化,不须要显式实现Serializable接口;c)当一个对象的实例变量引用其余对象,序列化该对象时也把引用对象进行序列化;3. 并不是全部的对象均可以序列化,至于为何不能够,有不少缘由了,好比:1.安全方面的缘由,好比一个对象拥有private,public等field,对于一个要传输的对象,好比写到文件,或者进行rmi传输等等,在序列化进行传输的过程当中,这个对象的private等域是不受保护的。2. 资源分配方面的缘由,好比socket,thread类,若是能够序列化,进行传输或者保存,也没法对他们进行从新的资源分配,并且,也是没有必要这样实现。

 

2,反过来父类未实现Serializable,子类实现了,序列化子类实例的时候,父类的属性是直接被跳过不保存,仍是能保存但不能还原?(答案:值不保存)

 

解:父类实现接口后,全部派生类的属性都会被序列化。子类实现接口的话,父类的属性值丢失。

 

java中序列化之子类继承父类序列化

标识符

标识符能够包括这4种字符:字母、下划线、$、数字;开头不能是数字;不能是关键字

Integer i=new Integer(127);和Integer i=127;的区别

Integer i = 127的时候,使用Java常量池技术,是为了方便快捷地建立某些对象,当你须要一个对象时候,就去这个池子里面找,找不到就在池子里面建立一个。可是必须注意 若是对象是用new 建立的。那么无论是什么对像,它是不会放到池子里的,而是向堆申请新的空间存储。Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值在-128到127之间的数时才可以使用对象池。超过了就要申请空间建立对象了

    int i1=128;
    Integer i2=128;
    Integer i3=new Integer(128);//自动拆箱

    System.out.println(i1==i2);//true
    System.out.println(i1==i3);//true

    Integer i5=127;
    Integer i6=127;
    System.out.println(i5==i6);//true


    Integer i5=127;
    Integer ii5=new Integer(127);
    System.out.println(i5==ii5);//false

    Integer i7=new Integer(127);
    Integer i8=new Integer(127);
    System.out.println(i7==i8);//false

 

手写单例模式

最好的单例模式是静态内部类,不要写双重检验

private static class LazySomethingHolder {
  public static Something something = new Something();
}

public static Something getInstance() {
  return LazySomethingHolder.something;
}

 

为何线程通讯的方法wait(), notify()和notifyAll()被定义在Object类里?

Java的每一个对象中都有一个锁(monitor,也能够成为监视器) 而且wait(),notify()等方法用于等待对象的锁或者通知其余线程对象的监视器可用。在Java的线程中并无可供任何对象使用的锁和同步器。这就是为何这些方法是Object类的一部分,这样Java的每个类都有用于线程间通讯的基本方法

Java中wait 和sleep 方法比较

  1. 这两个方法来自不一样的类分别是Thread和Object

  2. 最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其余线程可使用同步控制块或者方法。

  3. wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep能够在任何地方使用(使用范围)

  4. sleep必须捕获异常,而wait,notify和notifyAll不须要捕获异常

  5. sleep方法属于Thread类中方法,表示让一个线程进入睡眠状态,等待必定的时间以后,自动醒来进入到可运行状态,不会立刻进入运行状态,由于线程调度机制恢复线程的运行也须要时间,一个线程对象调用了sleep方法以后,并不会释放他所持有的全部对象锁,因此也就不会影响其余进程对象的运行。但在sleep的过程当中过程当中有可能被其余对象调用它的interrupt(),产生InterruptedException异常,若是你的程序不捕获这个异常,线程就会异常终止,进入TERMINATED状态,若是你的程序捕获了这个异常,那么程序就会继续执行catch语句块(可能还有finally语句块)以及之后的代码。

  • 注意sleep()方法是一个静态方法,也就是说他只对当前对象有效,经过t.sleep()让t对象进入sleep,这样的作法是错误的,它只会是使当前线程被sleep 而不是t线程

  1. wait属于Object的成员方法,一旦一个对象调用了wait方法,必需要采用notify()和notifyAll()方法唤醒该进程;若是线程拥有某个或某些对象的同步锁,那么在调用了wait()后,这个线程就会释放它持有的全部同步资源,而不限于这个被调用了wait()方法的对象。wait()方法也一样会在wait的过程当中有可能被其余对象调用interrupt()方法而产生

 

hashCode和equals方法的关系

在有些状况下,程序设计者在设计一个类的时候为须要重写equals方法,好比String类,可是千万要注意,在重写equals方法的同时,必须重写hashCode方法。也就是说对于两个对象,若是调用equals方法获得的结果为true,则两个对象的hashcode值一定相等;若是equals方法获得的结果为false,则两个对象的hashcode值不必定不一样;若是两个对象的hashcode值不等,则equals方法获得的结果一定为false;若是两个对象的hashcode值相等,则equals方法获得的结果未知。

Object类中有哪些方法,列举3个以上(能够引导)

Object方法:equals()、toString()、finalize()、hashCode()、getClass()、clone()、wait()、notify()、notifyAll()

String s=new String("xyz")究竟建立String Object分为两种状况:

  1. 若是String常理池中,已经建立"xyz",则不会继续建立,此时只建立了一个对象new String("xyz");

  2. 若是String常理池中,没有建立"xyz",则会建立两个对象,一个对象的值是"xyz",一个对象new String("xyz")。

什么是值传递和引用传递

值传递

public class TempTest {

  private void test1(int a) {
    a = 5;
    System.out.println("test1方法中的a=" + a);
  }

  public static void main(String[] args) {
    TempTest t = new TempTest();
    int a = 3;
    t.test1(11);
    System.out.println("main方法中a=" + a);
  }

}

 

test1方法中的a=5 main方法中a=3 值传递:传递的是值的拷贝,传递后就互不相关了 引用传递:传递的是变量所对应的内存空间的地址

public class TempTest {
  private void test1(A a) {
    a.age = 20;
    System.out.println("test1方法中a=" + a.age);
  }

  public static void main(String[] args) {
    TempTest t = new TempTest();
    A a = new A();
    a.age = 10;
    t.test1(a);
    System.out.println("main方法中a=" + a.age);
  }
}

class A {
  public int age = 0;
}

 

test1方法中a=20 main方法中a=20 传递前和传递后都指向同一个引用(同一个内存空间) 若是不互相影响,方法是在test1方法里面新new一个实例就能够了

 

讲一下netty

netty经过Reactor模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,boss线程和work线程池,其中boss线程池的线程负责处理请求的accept事件,当接收到accept事件的请求,把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责请求的read和write事件

Nio的原理(同步非阻塞)

服务端和客户端各自维护一个管理通道的对象,咱们称之为 selector,该对 象能检测一个或多个通道(channel)上的事件。咱们以服务端为例,若是服务 端的 selector 上注册了读事件,某时刻客户端给服务端送了一些数据,阻塞 I/O 这时会调用 read()方法阻塞地读取数据,而 NIO 的服务端会在 selector 中添加 一个读事件。服务端的处理线程会轮询地访问 selector,若是访问 selector 时发 现有感兴趣的事件到达,则处理这些事件,若是没有感兴趣的事件到达,则处 理线程会一直阻塞直到感兴趣的事件到达为止。

 


watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

 

缓冲区Buffer、通道Channel、选择器Selector

缓冲区Buffer

  • 缓冲区其实是一个容器对象,更直接的说,其实就是一个数组,在NIO库中,全部数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,它也是写入到缓冲区中的;任什么时候候访问 NIO 中的数据,都是将它放到缓冲区中。而在面向流I/O系统中,全部数据都是直接写入或者直接将数据读取到Stream对象中。

 

通道Channel

  • 通道是一个对象,经过它能够读取和写入数据,固然了全部数据都经过Buffer对象来处理。咱们永远不会将字节直接写入通道中,相反是将数据写入包含一个或者多个字节的缓冲区。一样不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。通道与流的不一样之处在于 通道是双向 的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类,好比 InputStream 只能进行读取操做,OutputStream 只能进行写操做),而通道是双向的,能够用于读、写或者同时用于读写。

 

选择器(Selector )

  • NIO 有一个主要的类 Selector,这个相似一个观察者,只要咱们把须要探知 的 socketchannel 告诉 Selector,咱们接着作别的事情, 当有事件发生时,他会 通知咱们,传回一组 SelectionKey, 咱们读取这些 Key, 就会得到咱们刚刚注册 过的 socketchannel, 而后,咱们从这个 Channel 中读取数据,放心,包准能 够读到,接着咱们能够处理这些数据。

  • Selector 内部原理实际是在作一个 对所注册的 channel 的轮询访问,不断 地轮询,一旦轮询到一个 channel 有所注册的事情发生,好比数据来了,他就 会站起来报告, 交出一把钥匙,让咱们 经过这把钥匙来读取这个 channel 的内 容。

 

BIO和NIO的区别

  1. BIO:同步阻塞式IO,服务器实现模式为一个链接一个线程,即客户端有链接请求时服务器端就须要启动一个线程进行处理,若是这个链接不作任何事情会形成没必要要的线程开销,固然能够经过线程池机制改善。

  2. NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的链接请求都会注册到多路复用器上,多路复用器轮询到链接有I/O请求时才启动一个线程进行处理。

NIO的selector做用

Selector(选择器)是Java NIO中可以检测一到多个NIO通道,并可以知晓通道是否为诸如读写事件作好准备的组件。这样,一个单独的线程能够管理多个channel,从而管理多个网络链接。

 

为了实现Selector管理多个SocketChannel,必须将具体的SocketChannel对象注册到Selector,并声明须要监听的事件(这样Selector才知道须要记录什么数据),一共有4种事件:

 

  1. connect:客户端链接服务端事件,对应值为SelectionKey.OP_CONNECT(8)

  2. accept:服务端接收客户端链接事件,对应值为SelectionKey.OP_ACCEPT(16)

  3. read:读事件,对应值为SelectionKey.OP_READ(1)

  4. write:写事件,对应值为SelectionKey.OP_WRITE(4)

 

每次请求到达服务器,都是从connect开始,connect成功后,服务端开始准备accept,准备就绪,开始读数据,并处理,最后写回数据返回。

因此,当SocketChannel有对应的事件发生时,Selector均可以观察到,并进行相应的处理。

相关文章
相关标签/搜索