一些面试基本知识(JAVA篇一)


写在前面:准备面试已经一段时间了,这期间看了不少知识点,刷了不少题目,但是没怎么记录。今天上byr时无意间看到学姐整理的一份面试记录,感觉很不错。所以让我也产生了记录面试的念头,首先就从面试前的一些基础知识上开始吧,这些都是最基本的概念,还是有必要做一个全面的总结。

附byr连接:https://bbs.byr.cn/#!article/Job/1877924?p=1


JAVA

java内存,堆栈特点与存放对象,值传递址传递(此处有坑),虚拟机GC算法原理

参考博客、文章:
http://www.cnblogs.com/perfy/archive/2012/10/16/2726039.html
http://www.jb51.net/article/77361.htm
http://guhanjie.iteye.com/blog/1683637

栈: 存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new出来的对象)或者常量池中(字符串常量对象存放在常量池中。)==存取速度比堆要快,仅次于寄存器==

:存放所有new出来的对象,==堆的优势是可以动态地分配内存大小==,生存期也不必事先告诉编译器

静态域: 存放静态成员(static定义的)

常量池: 存放字符串常量和基本类型常量(public static final)

这里我们主要关心栈,堆和常量池,对于栈和常量池中的对象可以共享,对于堆中的对象不可以共享。==栈中的数据大小和生命周期必须是确定的,当没有引用指向数据时,这个数据就会消失。堆中的对象由垃圾回收器负责回收,因此大小和生命周期不需要确定,具有很大的灵活性。==

在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。 当在一段代码块定义一个变量时,Java在栈中为这个变量分配内存空间,当该变量退出其作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。而数组和对象本身在堆中分配,即使程序运行到使用new产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。==这也是 Java 比较占内存的原因==。

参数是按值而不是按引用传递的说明==Java应用程序有且仅有的一种参数传递机制,即按值传递。== 值传递的精髓是:传递的是存储单元中的内容,而非地址或者引用! 在Java应用程序中永远不会传递对象,而只传递对象引用。因此是按引用传递对象。Java应用程序按引用传递对象这一事实并不意味着Java应用程序按引用传递参数。参数可以是对象引用,而Java应用程序是按值传递对象引用的。

按值传递和按引用传递。按值传递意味着当将一个参数传递给一个函数时,函数接收的是原始值的一个副本。因此,如果函数修改了该参数,仅改变副本,而原始值保持不变。按引用传递意味着当将一个参数传递给一个函数时,函数接收的是原始值的内存地址,而不是值的副本。因此,如果函数修改了该参数,调用代码中的原始值也随之改变。

//定义了一个改变参数值的函数  
public static void changeValue(int x) {  
x = x *2;  
}  
//调用该函数  
int num = 5;  
System.out.println(num);  
changeValue(num);  
System.out.println(num);  

答案显而易见,调用函数changeValue()前后num的值都没有改变。

由此做一个引子,我用图表描绘一个值传递的过程:

num作为参数传递给changeValue()方法时,是将内存空间中num所指向的那个存储单元中存放的值,即”5”,传送给了changeValue()方法中的x变量,而这个x变量也在内存空间中分配了一个存储单元,这个时候,就把num的值5传送给了这个存储单元中。此后,在changeValue()方法中对x的一切操作都是针对x所指向的这个存储单元,与num所指向的那个存储单元没有关系了!自然,在函数调用之后,num所指向的存储单元的值还是没有发生变化,这就是所谓的“值传递”!值传递的精髓是:==传递的是存储单元中的内容,而非地址或者引用!==

接下来,就来看java中的对象参数是怎么传递的:

同样,先给出一段代码:

... ...  
class person {  
public static String name = "Jack";  
... ...  
}  
... ...  
//定义一个改变对象属性的方法  
public static void changeName(Person p) {  
p.name = "Rose";  
}  
... ...  
public static void main(String[] args) {  
//定义一个Person对象,person是这个对象的引用  
Person person = new Person();  
//先显示这个对象的name属性  
System.out.println(person.name);  
//调用changeName(Person p)方法  
changeName(person);  
//再显示这个对象的name属性,看是否发生了变化  
System.out.println(person.name);  
}  

第一次显示:“Jack”

第二次显示:“Rose”

这里就有一个问题了,++当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?++

答:是值传递。Java编程语言只有值传递参数。==当一个对象实例作为一个参数被传递到方法中时,参数的值就是该对象的引用一个副本。指向同一个对象,对象的内容可以在被调用的方法中改变,但对象的引用(不是引用的副本)是永远不会改变的。==

为什么这里是“值传递”,而不是“引用传递”?

我还是用图表描绘比较能解释清楚:

主函数中new 了一个对象Person,实际分配了两个对象:新创建的Person类的实体对象,和指向该对象的引用变量person。

【注意:在java中,新创建的实体对象在堆内存中开辟空间,而引用变量在栈内存中开辟空间】

正如如上图所示,左侧是堆空间,用来分配内存给新创建的实体对象,红色框是新建的Person类的实体对象,000012是该实体对象的起始地址;而右侧是栈空间,用来给引用变量和一些临时变量分配内存,新实体对象的引用person就在其中,可以看到它的存储单元的内容是000012,记录的正是新建Person类实体对象的起始地址,也就是说它指向该实体对象。

这时候,好戏上台了:

调用了changeName()方法,person作为对象参数传入该方法,但是大家特别注意,它传入的是什么!!!person引用变量将自己的存储单元的内容传给了changeName()方法的p变量!也就是将实体对象的地址传给了p变量,从此,在changeName()方法中对p的一切操作都是针对p所指向的这个存储单元,与person引用变量所指向的那个存储单元再没有关系了!

回顾一下上面的一个值传递的例子,值传递,就是将存储单元中的内容传给调用函数中的那个参数,这里是不是异曲同工,是所谓“值传递”,而非“引用传递”!!!

那为什么对象内部能够发生变化呢?

那是因为:p所指向的那个存储单元中的内容是实体对象的地址,使得p也指向了该实体对象,所以才能改变对象内部的属性!
这也是我们大多数人会误以为是“引用传递”的终极原因!!!

至于最后一个问题,==JAVA虚拟机的GC算法==,有一篇博客写的特别详细,这里就不转述了,直接上原文连接:http://www.cnblogs.com/smyhvae/p/4744233.html

Java创建对象的过程

Java创建对象的过程 http://www.cnblogs.com/CZDblog/p/5589379.html

Java是一门面向对象的编程语言,在Java程序运行过程中每时每刻都有对象被创建出来。在语言层面上,创建对象通常仅仅是一个new关键字而已,而在虚拟机中,对象的创建又是怎样一个过程呢?

一、检测类是否被加载

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

二、为新生对象分配内存

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定。

三、初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

四、进行必要的设置

接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。

五、执行init方法

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚开始,方法还没有执行,所有的字段都还为零。所以一般来说,执行new指令之后会接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

总结一下上面所说的,创建一个对象的过程就是:

检测类是否被加载没有加载的先加载→为新生对象分配内存→将分配到的内存空间都初始化为零值→对对象进行必要的设置→执行方法把对象进行初始化

Set,Map,List等的原理特性,尤其是ArrayList,HashMap,HashTable,ConcurrentHashMap等,是否线程安全?如何解决hash冲突?等等

参考博客、文章:http://blog.csdn.net/xiao__gui/article/details/8934832
http://blog.csdn.net/yaerfeng/article/details/7254734
http://smallbug-vip.iteye.com/blog/2275743

这个问题涉及的知识点很广,针对各大数据集的详解请见http://blog.csdn.net/u011860731/article/details/48717123

这里记一下线程安全和非线程安全:

ArrayList是非线程安全的,Vector是线程安全的;HashMap是非线程安全的,HashTable是线程安全的;StringBuilder是非线程安全的,StringBuffer是线程安全的。

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

非线程安全是指多线程操作同一个对象可能会出现问题。而线程安全则是多线程操作同一个对象不会有问题。

线程安全必须要使用很多synchronized关键字来同步控制,所以必然会导致性能的降低。

比如一个 ArrayList 类,在添加一个元素的时候,它可能会有两步来完成:1. 在 Items[Size] 的位置存放此元素;2.增大Size的值。在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;

而如果是在多线程情况下,比如有两个线程,线程A先将元素存放在位置 0。但是此时CPU调度线程A暂停,线程B得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加Size的值。那好,现在我们来看看ArrayList的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。

非线程安全!=不安全

有人在使用过程中有一个不正确的观点:我的程序是多线程的,不能使用ArrayList要使用Vector,这样才安全。

非线程安全并不是多线程环境下就不能使用。注意我上面有说到:多线程操作同一个对象。注意是同一个对象。比如最上面那个模拟,就是在主线程中new的一个ArrayList然后多个线程操作同一个ArrayList对象。如果是每个线程中new一个ArrayList,而这个ArrayList只在这一个线程中使用,那么肯定是没问题的。

ArrayList、LinkedList、Vector的区别

http://blog.csdn.net/renfufei/article/details/17077425

  1. ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。

  2. 对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。

  3. 对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。
  4. Vector 和ArrayList类似,但属于强同步类。如果你的程序本身是线程安全的(thread-safe,没有在多个线程之间共享同一个集合/对象),那么使用ArrayList是更好的选择。
  5. 数据增长:当需要增长时,Vector默认增长为原来一培,而ArrayList却是原来的一半
  6. LinkedList 还实现了 Queue 接口,该接口比List提供了更多的方法,包括 offer(),peek(),poll()等.

String、StringBuffer与StringBuilder的区别
1. String用于存放字符的数组被声明为final的,因此只能赋值一次,不可再更改。
2. 要是需要多次更改,需要用到StringBulider或者StringBuffer,两者不同点在于StringBuffer是线程安全的。
3. StringBulider转为String:
String m = sb.toString();
String转StringBuilder:
StringBuilder sb = new StringBuilder(m);

HashMap和HashTable的区别。

http://blog.csdn.net/sadjladjlas/article/details/51210479

  1. 历史原因:Hashtable是基于陈旧的Dictionary类的,HashMap是Java 1.2引进的Map接口的一个实现
  2. 同步性:Hashtable是线程安全的,也就是说是同步的,而HashMap是线程序不安全的,不是同步的
  3. 只有HashMap可以让你将空值作为一个表的条目的key或value

怎么解决Hash冲突?

http://blog.csdn.net/lightty/article/details/11191971

  1. 开放定址法

用开放定址法解决冲突的做法是:当冲突发生时,使用某种探查(亦称探测)技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探查到开放的 地址则表明表中无待查的关键字,即查找失败。

按照形成探查序列的方法不同,可将开放定址法区分为线性探查法、线性补偿探测法、随机探测等。

  1. 拉链法

拉链法解决冲突的做法是:将所有关键字为同义词的结点链接在同一个单链表中。 若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于 1,但一般均取α≤1。

与开放定址法相比,拉链法有如下几个优点:

①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;

②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;

③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;

④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。

拉链法的缺点是:==指针需要额外的空间==,故当结点规模较小时,开放定址法较为节省空间

设计模式,尤其是单例模式的饿汉子饱汉子各种写法,被考过n多次

参考博客、文章 http://www.cnblogs.com/wabi87547568/p/5280905.html

JAVA总共有23种设计模式,而常见的设计模式有6种(其实是自己只熟悉这6种):

至于常见的面试题,请直接看 http://www.importnew.com/12649.html

1. 单例模式

http://blog.csdn.net/jason0539/article/details/23297037

Java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍两种 ==:懒汉式单例、饿汉式单例。==

单例模式有以下特点:

  • 单例模式只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。

单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler, 以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。

懒汉式单例

//懒汉式单例类.在第一次调用的时候实例化自己   
public class Singleton{
    private static Singleton instance=null;
    public static synchronized Singleton getInstance(){
        if(instance==null){
               instance=new Singleton();
        }
        return instance;
    }
    private Singleton(){
    }
}

Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。

饿汉式单例

//饿汉式单例类.在类初始化时,已经自行实例化   
public class Singleton1 {  
    private Singleton1() {}  
    private static final Singleton1 single = new Singleton1();  
    public static Singleton1 getInstance() {  
        return single;  
    }  
}  

饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。

饿汉式和懒汉式区别

从名字上来说,饿汉和懒汉,

饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了,

而懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例。

另外从以下两点再区分以下这两种方式:

1、线程安全:

饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,

懒汉式本身是非线程安全的,为了实现线程安全就在getInstance上加同步锁。

2、资源加载和性能:

饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成,

而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。

2. 工厂模式

http://baike.baidu.com/link?url=Zc_1AhC4Ty85THc1iXJigk_i_1ePR9tUnj7uKT2PnKBJT4-awOWl9UlXHEPNuDpuF3yex0r_cT_q0uBUn4iEtZO3rwsbTVhpwrIIrz7_ZMbco0VnkUUW0NNlrw770tio
http://blog.csdn.net/jason0539/article/details/23020989

模式的问题:你如何能轻松方便地构造对象实例,而不必关心构造对象实例的细节和复杂过程呢?

解决方案: 建立一个工厂来创建对象

工厂模式的两种情况

1.在编码时不能预见需要创建哪种类的实例。

2.系统不应依赖于产品类实例如何被创建、组合和表达的细节

工厂模式定义

我们以类Sample为例, 如果我们要创建Sample的实例对象:
Sample sample=new Sample();

可是,实际情况是,通常我们都要在创建sample实例时做点初始化的工作,比如赋值 查询数据库等。

首先,我们想到的是,可以使用Sample的构造函数,这样生成实例就写成:
Sample sample=new Sample(参数);

但是,如果创建sample实例时所做的初始化工作不是像赋值这样简单的事,可能是很长一段代码,如果也写入构造函数中,那你的代码很难看了(就需要Refactor重整)。

为什么说代码很难看,初学者可能没有这种感觉,我们分析如下,初始化工作如果是很长一段代码,说明要做的工作很多,将很多工作装入一个方法中,相当于将很多鸡蛋放在一个篮子里,是很危险的,这也是有悖于Java面向对象的原则。

面向对象的封装(Encapsulation)和分派(Delegation)告诉我们,尽量将长的代码分派“切割”成每段,将每段再“封装”起来(减少段和段之间耦合联系性),这样,就会将风险分散,以后如果需要修改,只要更改每段,不会再发生牵一动百的事情。

在本例中,首先,我们需要将创建实例的工作与使用实例的工作分开, 也就是说,让创建实例所需要的大量初始化工作从Sample的构造函数中分离出去。

这时我们就需要Factory工厂模式来生成对象了,不能再用上面简单new Sample(参数)。还有,如果Sample有个继承如MySample, 按照面向接口编程,我们需要将Sample抽象成一个接口.ISample是接口,有两个子类MySample 和HisSample .我们要实例化他们时,如下:

ISample mysample=new MySample();

ISample hissample=new HisSample();

随着项目的深入,Sample可能还会”生出很多儿子出来”, 那么我们要对这些儿子一个个实例化,更糟糕的是,可能还要对以前的代码进行修改:加入后来生出儿子的实例.这在传统程序中是无法避免的.
但如果你一开始就有意识使用了工厂模式,这些麻烦就没有了.

3. 建造者模式

http://baike.baidu.com/link?url=tFL4qDSIAnxxdBBsA7LnNhC0xqy0bJZkeyJ_UEEGHHIYUSO2UWLLVcQrbn6nCqf1PNpOv0qVm2DYkYqAh3z4X9Chv-6V3n4xXhNGMIFvuEMsmdscRJxbs3uTM6TMgQvaJReh3RJQRGXUMjq2fjR8ba

http://www.cnblogs.com/cbf4life/archive/2010/01/14/1647710.html

该模式其实就是说,一个对象的组成可能有很多其他的对象一起组成的,比如说,一个对象的实现非常复杂,有很多的属性,而这些属性又是其他对象的引用,可能这些对象的引用又包括很多的对象引用。封装这些复杂性,就可以使用建造模式。

定义

将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,这样的设计模式被称为建造者模式。

实用范围

相同的方法,不同的执行顺序,产生不同的事件结果时,可以采用建造者模式。

多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同时,则可以使用该模式。

产品类非常复杂,或者产品类中的调用顺序不同产生了不同的效能,这个时候使用建造者模式是非常合适。

在对象创建过程中会使用到系统中的一些其它对象,这些对象在产品对象的创建过程中不易得到时,也可以采用建造者模式封装该对象的创建过程。该种场景,只能是一个补偿方法,因为一个对象不容易获得,而在设计阶段竟然没有发觉,而要通过创建者模式柔化创建过程,本身已经违反设计最初目标。

4. 门面模式

http://www.cnblogs.com/zhenyulu/articles/55992.html

外部与一个子系统的通信必须通过一个统一的门面(Facade)对象进行,这就是门面模式。门面模式提供一个高层次的接口,使得子系统更易于使用。

就如同医院的接待员一样,门面模式的门面类将客户端与子系统的内部复杂性分隔开,使得客户端只需要与门面对象打交道,而不需要与子系统内部的很多对象打交道。

适用范围:

为一个复杂子系统提供一个简单接口

提高子系统的独立性

在层次化结构中,可以使用Facade模式定义系统中每一层的入口。

5. 策略模式

定义一系列的算法,把每一个算法封装起来, 并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。

把一个类中经常改变或者将来可能改变的部分提取出来,作为一个接口,然后在类中包含这个对象的实例,这样类的实例在运行时就可以随意调用实现了这个接口的类的行为。
比如定义一系列的算法,把每一个算法封装起来, 并且使它们可相互替换,使得算法可独立于使用它的客户而变化。这就是策略模式。

适用情况

许多相关的类仅仅是行为有异。

“策略”提供了一种用多个行为中的一个行为来配置一个类的方法。即一个系统需要动态地在几种算法中选择一种。

当一个应用程序需要实现一种特定的服务或者功能,而且该程序有多种实现方式时使用。一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现。将相关的条件分支移入它们各自的Strategy类中以代替这些条件语句。

6. 观察者模式

观察者模式是软件设计模式的一种。有时被称作发布/订阅模式,观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。此种模式通常被用来实现事件处理系统。

你可能感兴趣的:(java-面试,面试,java,栈,JAVA)