java面试

来源:互联网 发布:丁丁软件如何使用 编辑:程序博客网 时间:2024/06/06 09:29

快一年没看过java,面试的时候被虐成狗,还是复习一下吧
内容有些是从书上看到,有些是百度到的。

JVM

Java运行时的数据区域:
1. 程序计数器
a. 当前线程所执行的字节码的行号指示器,通过改变本区域的值来选取下一条要执行的指令
b. 如果线程正在执行的是java方法,那么计数器记录的是正在执行的虚拟机字节码指令地址
c. 如果正在执行的是native方法,那么本区域为空
2. Java虚拟机栈
a. 虚拟机栈描述的是java方法执行的内存模型,每个方法执行时会创建一个栈,用于存储局部变量表,操作数栈,动态链接,方法出口等信息
b. 本区域定义了2种异常:
i. 当线程请求的栈深度大于虚拟机所允许的深度时,抛出stackoverflowError异常
ii. 申请内存时,动态扩展后还是无法申请到足够的内存,抛出OutofMemoryError异常
3. 本地方法栈
a. 为native方法服务,和Java虚拟机栈一样
4. Java堆
a. 被所有线程共享的一块区域,用于存放对象的实例
b. 是GC的主要区域
5. 方法区
a. 各个线程共享的内存区域,用于存储虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码

JAVA classLoader

摘抄:http://dlevin.iteye.com/blog/772604
一个class文件从解析到生成对象,这些工作时classloader做的。
在java内部,有三种类型的classloader:bootstrap classloader, extension classloader, system classloader。
它们是链式的,是父类、子类、孙类。。。
bootstrap classloader —>extension classloader —> system classloader

Bootstrap ClassLoader
它是用于在启动jvm时加载类,使得jvm能正常工作,它是用native代码实现的,最早被创建,处于最底层。它将搜索并加载java的核心库,例如java.lang,java.io等

Extension ClassLoader
它会搜索并加载特定的标准扩展目录,例如sun的jvm中,扩展目录是%JRE_HOME% \lib\ext,用于扩展和共享,应用程序厂商将部分共享库放在这里,而不是各自程序的目录下多份拷贝。

system ClassLoader
它是用来搜索classpath中配置的目录和Jar文件

类的加载过程:

一个类是如何被加载的呢?在java中是通过“代理模型”来决定使用哪个classloader来加载类。
首先,系统默认使用system classloader来加载该类;
然后,system classloader 会将加载行为代理给其parent去加载,只有其parent不能加载该类时,system classloader才去搜索classpath中的目录和jar文件来加载类,如果找到对应的.class文件,则加载,否则抛出classnotfloundexception。

垃圾回收机制

  1. 怎么判断对象已死
    a. 引用计数法
    i. 给每个对象添加一个引用计数器,每当一个地方引用它,就给计数器加1,如果计数器为0,那么这个对象已死(互相引用时无法回收)
    ii. 可达性分析算法
    从GC Root出发作为起点,从这些起点向下搜索,搜索走过的路径成为引用链,当一个对象到GC Root没有任何引用链相连时,证明此对象不可用
    可以作为GC Root的有:
    虚拟机栈中引用的对象
    方法去中类静态属性引用的对象
    方法区常量引用的对象
    本地方法栈中JNI 引用的对象
  2. 垃圾回收算法
    a. 标记-清除法
    i. 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
    b. 复制算法
    i. 将内存分为2块,每次只使用其中的一块,当这一块内存快用完了,就将活着的对象复制到另一块,然后再把已经使用的内存空间一次清理掉
    c. 标记-整理算法
    i. 第一步标记,第二步是将所有存活的对象向一端移动,然后直接清理掉边界以外的内存
    d. 分代收集

内存分配和回收机制可以概括为: 分代分配,分代回收
3. 内存分配算法 & 垃圾回收算法
根据对象的存活时间,分为年轻代,年老代,永久代
年轻代:对象刚创建时,首先将它分配到年轻代,年轻代有分为3个区域,一个区域为eden区,一个区域为survivor0区,还有一个区域为survivor1区。
对象首先被分配到eden区,当eden区满时,运行GC,将存活的对象复制到survivor0区,当survivor0区满时,运行GC,将存活的对象复制到survivor1区,此时,eden区和survivor0区都是无用对象,可清空。
年老代:当对象在年轻代中存活了足够长的时间没有被清理掉,会被复制到年老代
年老代的GC用标记整理法
永久代:永久代是方法区,可以回收的是常量池中的常量 还有无用的类信息
常量回收:没有引用
可以回收的类:所有的实例被回收;classloader被回收;类对象的Class对象没有被引用(即没有通过反射引用该类的地方)

基础知识

面向对象的特征: 抽象、继承、封装。
封装是指把客观事物封装成抽象的类
继承是 使用现有类的所有功能,在无需编写原来类的情况下对功能进行扩展
多态是允许将子类类型的指针赋值给父类类型的指针。

多态:实现多态的两种方式: 覆盖和重载
覆盖:是指子类重新定义父类中的方法或变量。注意可以改变其访问权限,但只可放大不可缩小。
重载:是指允许存在多个同名函数,而参数表不同。

java的数据类型:
数据类型
byte:1个字节 char:2个字节
short: 2个字节 boolean:一位
Int:4个字节 float:4个字节
long: 8个字节 double:8个字节

int 和Integer的区别:

int 是基本数据类型,直接存数值;
integer是java为int提供的封装类
他们之间可以用自动装箱和拆箱互相转换(是将基本数据类型和对象类型互相转换)比如:
Integer num = 10; //等价于下面: 这是自动装箱
Integer num = new Integer(10);

Integer num = 10;
int num1 = num; //这是拆箱。

string和stringbuffer的区别:

string是一个不可变对象,一旦创建,就不能修改它的值。如果要修改已存在的string对象,则需要重新创建一个新的对象,然后把新的值保存进去。String是final类,不能被继承
stringbuffer是一个可变对象,当对它进行修改的时候不会像string那样重新建立对象,只能通过构造函数来建立
创建一个空的stringbuffer:
StringBuffer s = new StringBuffer();
创建一个带内容的stringbuffer:
StringBuffer s = new StringBuffer(“abc”);
StringBuffer对象和String对象之间的互转的代码如下:
String s = “abc”;
StringBuffer sb1 = new StringBuffer(“123”);
StringBuffer sb2 = new StringBuffer(s); //String转换为StringBuffer
String s1 = sb1.toString(); //StringBuffer转换为String

StringBuffer和StringBuilder的区别:
StringBuffer是线程安全的
StringBuilder不是线程安全的,在单线程的环境下,性能比StringBuffer好。

static关键字

Static 变量
用static修饰的变量叫静态变量。静态变量不需要new出对象引用来调用,它可以直接用类名直接调用。只要类被加载了,就可以通过类名去进行访问
静态变量在内存空间中只有一份,也永远只有一份。大家共享
非静态变量只要new出一个对象引用就会在内存中分配一份空间给它。
静态变量存在栈空间,非静态变量存在堆空间

static代码块
static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次

static方法
静态方法,不依赖于对象就可以访问
注意: static方法中不能访问非静态的方法/变量

final关键字

final类不能被继承,没有子类,final类中的方法默认是final的。

final方法不能被子类的方法覆盖,但可以被继承。
final不能用于修饰构造方法。
使用final方法的原因有:
1.将方法锁定,防止任何继承类修改它;
2.高效,编译器在遇到final方法时会转入内嵌机制,提高执行效率。

final成员变量表示常量,只能被赋值一次,赋值后值不再改变。可以在定义的时候不赋值,但是编译器会保证在使用前它会被赋值。

static和final一块用:
用来修饰变量和方法,可以理解为 “全局常量”
对于变量,一旦给值就不可修改,且可通过类名访问;
对于方法,表示不可覆盖,且可通过类名直接访问。

传值和传引用

Java方法的参数是简单类型的时候,是按值传递的, 将参数的值做了一个拷贝传进方法
基本类型: int long double float byte boolean char

引用是一种数据类型,保存了对象在内存中的地址,这种类型即不是我们平时所说的简单数据类型也不是类实例(对象);不同的引用可能指向同一个对象,换句话说,一个对象可以有多个引用,即该类类型的变量。

对于对象型的变量,Java传的是引用的副本

Java clone()

因为java对对象型的变量在拷贝的时候是传引用,所以在需要拷贝出另一个与之前对象不相关的对象时,必须要使用深拷贝。
Object在对某个对象实施Clone时对其是一无所知的,它仅仅是简单地执行域对域的copy,这就是Shallow Clone。
如果要拷贝一个全新的对象,需要深拷贝。
如何使用clone功能:
1.希望能实现clone功能的CloneClass类需要实现了Cloneable接口
2.重载clone方法,在clone()方法中调用了super.clone(),Clone()方法是个Protected方法,所以要用clone()必须要继承Object类,为了让其他类调用这个clone方法,要将clone()设为public的
3.Object类的clone()一个native方法,native方法的效率一般来说都是远高于java中的非native方法。这也解释了为 什么要用Object中clone()方法而不是先new一个类,然后把原始对象中的信息赋到新对象中,虽然这也实现了clone功能
eg.

class Employee implements Cloneable  {          public Object clone() throws CloneNotSupportedException          {              Employee cloned = (Employee) super.clone();              cloned.hireDay = (Date) hireDay.clone()              return cloned;          }  }  

这个使用了深拷贝,因为Employee有一个成员变量是date,是对象型变量,为了使它深拷贝,在clone的时候也要将它拷贝一份

java异常

异常分类
throwable类是java所有错误或异常的超类,它有2个子类:Error和Exception

Error:合理的应用程序不应该试图捕获的严重问题,是已经大到不能处理了,
只能交给jvm处理,比如 OutOfMemoryError

Exception:指出了合理的应用程序想要捕获的条件,分为两种:
Runtime Exception,(非检查异常) 比如nullpointerException,一般是由程序逻辑错误引起的,应从逻辑的角度来尽量避免这类异常
非Runtime Except ion,(检查异常)是必须处理的异常,如果不处理,那么编译不会通过,比如IOException

Try{ }
Catch(异常类型 异常名){
}
finally{ }
finally代码块总是在方法返回前执行

throw 用于方法体内部,用来抛出一个throwable类型的异常。
Throws 用于方法体外部,用来申明方法可能会抛出某些异常。

void doA(int a) throws IOException,{           try{                 ......           }catch(Exception1 e){              throw e;           }catch(Exception2 e){              System.out.println("出错了!");           }           if(a!=b)              throw new  Exception3("自定义异常");}

序列化

将一个实现了Serializable接口的对象转换成一组byte,日后用到这个对象的时候可以把这些byte数据恢复出来

== 和equals() hashcode()

基本数据类型:比较用==, 比较他们的值
复合数据类型:用==比较时,比较的是它们在内存中存放的地址,除非是同一个new出来的对象,他们的比较后果为true,否则为false。object基类中定义了equals()方法,它的初始行为是比较它们的内存地址(就是和==功能一样),但在有些类库中覆盖了,比如String类的equals()方法是比较它们的内容。

Hashcode(),它是一个native方法,Object类提供的默认实现确实保证每个对象的hash码不同(在对象的内存地址基础上经过特定算法返回一个hash码),在hashtable中,它是用来在查找的时候确定对象的存储地址的

HashMap, HashTable,HashSet

哈希算法: 将任意长度的二进制值映射为较短的固定长度的二进制值 里面应该用到了位与运算
集合存储存的不是Java对象,而是对象引用的集合
hash算法

新插入的值,总是在离数组最近的位置,所以先插入的元素,访问时间比后插入的长
相同的hashcode,会存在同一个bucket,然后会遍历链表,用equals()方法来比较,找到正确的节点

HashMap 是 Map 接口的常用实现类,HashSet 是 Set 接口的常用实现类

HashSet 和 HashMap 之间有很多相似之处,
对于 HashSet 而言,系统采用 Hash 算法决定集合元素的存储位置,这样可以保证能快速存、取集合元素;
对于 HashMap 而言,系统 key-value 当成一个整体进行处理,系统总是根据 Hash 算法来计算 key-value 的存储位置,这样可以保证能快速存、取 Map 的 key-value 对

hashmap是非线程安全的,可以接受key 为null的情况,在默认的情况下,hashmap是非同步的,所以在多线程使用hashmap时,需要额外的同步时间
即如果hashmap里没发现key,或者发现key为null,那么会返回Null,可以用cotainKey()来区别
hashtable是线程同步的,多个线程可以共享一个hashtable, 在多线程的情况下,可以直接使用hashtable

hashmap的迭代器是fail-fast迭代器,当一个线程在访问hashmap,其他线程改变hashmap的时候,将抛出异常

HashSet实现了Set接口,它不允许集合中有重复的值,当我们提到HashSet时,第一件事情就是在将对象存储在HashSet之前,要先确保对象重写equals()和hashCode()方法,这样才能比较对象的值是否相等,以确保set中没有储存相等的对象。如果我们没有重写这两个方法,将会使用这个方法的默认实现

Map.Entry是Map内部定义的一个接口,用于保存key-value

java多线程

定义线程,两种方法:
1. 继承java.lang.Thread类,重写public void run()方法
2. 实现java.lang.Runnable接口,启动该线程,即使用t.start()将导致在独立执行的线程中调用对象的run()方法
在调用start方法之前,线程是出于新状态中,新状态是指有一个thread对象,但是还没有一个真正的线程。
调用start方法之后,发生了以下事情:
启动新的执行线程(具有新的调用栈)。
该线程从新状态转移到可运行状态。
当该线程获得机会时,其目标的run()方法执行。

获取当前线程的方法:thread.currentThread()
当线程的run()方法结束时,该线程执行完成。

线程的调度是JVM的一部分,在一个CPU的机器上上,实际上一次只能运行一个线程。一次只有一个线程栈执行。JVM线程调度程序决定实际运行哪个处于可运行状态的线程。众多可运行线程中的某一个会被选中做为当前线程。可运行线程被选择运行的顺序是没有保障的
线程栈是指某时刻时内存中线程调度的栈信息,当前调用的方法总是位于栈顶。(感觉类似于程序计数器)

线程状态:
新线程: 线程对象已经创建,还没有在其上调用start()方法。
可运行状态: 线程有资格运行,但调度程序还没有将它选定为运行线程时。
运行状态:被调度程序在线程池中宣威当前线程,是线程进入运行状态的唯一方式。
等待、阻塞、睡眠:线程是活的,但是得需要到特定条件才能返回到可运行状态。
死亡态:线程的run()方法执行完成,也许线程是活的,但是它已经不是一个单独执行的线程了。一旦死亡,就不能复生

线程的睡眠: thread.sleep(3)
线程睡眠到期自动苏醒,并返回到可运行状态,不是运行状态

线程的让步yield()
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。

暂停当前正在执行的线程对象,并执行其他线程。线程是存在优先级的1-10之间。线程调度是基于优先级的。当两个线程具有相同优先级时,jvm自由选择。
如果池内每个线程具有相同的优先级所以此时调度程序的操作有两种:1是线程运行时阻塞或运行完成,2是时间分片,为池内每个线程提供均等的运行机会。

Join()
Join()是让一个线程B加入到另一个线程A的尾部,在A执行完毕之前,B不能工作。

同步与锁定:
java的每个对象都有一个内置锁,当程序运行到非static的synchronized同步方法时,自动获得当前实例有关的锁,当程序运行到synchronized方法时,对象锁才起作用。

锁和同步的要点:
1. 只能同步方法,不能同步变量和类
2. 每个对象只有一个锁,当提到同步时,应该清楚在哪个对象上同步;
3. 类中可以同时拥有同步和非同步的方法;
4. 如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程来执行这个方法,另一个需等到锁被释放。即 : 如果一个线程在对象上获得一个锁,就没有任何其他线程可以访问该对象的任何一个同步方法。
5. 如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。
在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁

如果要同步静态方法,那么需要锁整个类。

线程安全类:
当一个类可以很好的同步以保护它的数据时,这个类就成为:“线程安全的”

0 0
原创粉丝点击