人生第一次面试,回答得特别特别烂,我觉得面试官肯定对我很无语。但是真的很感谢这个面试官,让我有了一次对我面试帮助很大很大的面经。这次面试之后,记录了面试的要点,进行了针对性查缺补漏,对我后面的面试帮助很大。
不要畏惧面试,多面试涨涨经验总是好的。不要有自己在浪费面试官时间的想法,面试官在面试你的时候也是在完成他的任务(公司不可能让他白干活),你要学会合理利用资源,利用面试长经验。当你面试得足够多了,准备得足够充分了,offer自然也就到手了。
加油!
1. Java四种访问修饰符
- public: Java语言中访问限制最宽的修饰符,一般称之为“公共的”。被其修饰的类、属性以及方法不
仅可以跨类访问,而且允许跨包(package)访问。 - private: Java语言中对访问权限限制的最窄的修饰符,一般称之为“私有的”。被其修饰的类、属性以
及方法只能被该类的对象访问,其子类不能访问,更不能允许跨包访问。 - protected: 介于public 和 private 之间的一种访问修饰符,一般称之为“保护形”。被其修饰的类、
属性以及方法只能被类本身的方法及子类访问,即使子类在不同的包中也可以访问。 - default:即不加任何访问修饰符,通常称为“默认访问模式“。该模式下,只允许在同一个包中进行访
问。
2.Java常见的基本类型
8种 = 非数值型2种 + 数值型6种
- 数值型
- byte
- short
- int
- long
- float
- double
- 非数值型
- boolean
- char
3. Java值传递和引用传递
- 基本数据类型传值,对形参的修改不会影响实参;
- 引用类型传引用
- 形参和实参指向同一个内存地址,所以对参数的修改会影响到实际的对象;
- 形参和实参指向不同内存地址的,则对参数的修改不会影响到实际的对象;
String 是引用类型
Integer m1 = 127;
Integer m2 = 127;//当在-128~127范围内返回true,否则返回false
int x1 = 500;
int x2 = 500;//全都返回true 存在栈中,栈中的数据可以共享
Integer n1 = new Integer(22);//全都返回false,不同的对象对应不同的地址
Integer n2 = new Integer(22);
System.out.println(m1 == m2);
System.out.println(x1 == x2);
System.out.println(n1 == n2);
// 输出:true,true,false
运行时常量池一直在方法区(Method Area),里面包含了每一个.class文件中的常量池中的内容。而字符串池在Java 7之前保存在方法区,在Java 7之后保存在堆上。
-
以上提到的几种基本类型包装类均实现了常量池技术,但它们维护的常量仅仅是【-128~127】这个范围内的常量,如果常量值超过这个范围,就会从堆中创建对象,不再从常量池中取。比如,把上边例子改成Integer i1 = 400; Integer i2 = 400;,很明显超过了127,无法从常量池获取常量,就要从堆中new新的Integer对象,这时i1和i2就不相等了。
-
String类型也实现了常量池技术,但是稍微有点不同。String型是先检测常量池中有没有对应字符串,如果有,则取出来;如果没有,则把当前的添加进去。
4. Java单例
(1)懒汉式–线程不安全【不可用】
- 指向自己的实例的私有静态引用
- 私有构造方法
- 静态共有方法来获取该实例
public class Singleton{
private static Singleton instance ;
private Singleton(){
}
public static Singleton getInstance(){
if(instance == null) { // 被动创建,在真正需要使用时才去创建
instance = new Singleton();
}
return instance;
}
}
(2)懒汉式–线程安全 【不可用】
缺点:效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低要改进。
public class Singleton{
private static Singleton instance ;
private Singleton(){
}
public static synchronized Singleton getInstance(){
if(instance == null) { // 被动创建,在真正需要使用时才去创建
instance = new Singleton();
}
return instance;
}
}
(3)饿汉式(静态代码块)【可用】
将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。
优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return instance; //直接使用已经创建好的实例
}
}
优点:饿汉模式天生是线程安全的,使用时没有延迟。
缺点:启动时即创建实例,启动慢,有可能造成资源浪费。
(4)双重检查【推荐使用】
进行了两次if (singleton == null)检查,这样就可以保证线程安全了。
这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象。
优点:线程安全;延迟加载;效率较高。
public class Singleton {
//对保存实例的变量添加volatile的修饰
private static volatile Singleton instance;
private Singleton(){
}
public static Singleton getInstance() {
if(instance == null) {
//假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,
//这时要保证不会产生多个实例。(所以下面要进行第二次验证)
synchronized (Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
5.static的用法
一定要提到类加载!!!
方便在没有创建对象的情况下来进行调用(方法/变量)。
static:静态的,用于修饰成员(成员变量,成员方法);
-
被static所修饰的变量或者方法会储存在数据共享区(静态存储区);
-
被static修饰后的类成员变量只有一份!
-
当成员被static修饰之后,就多了一种访问方式,除了可以被对象调用之外,还可以直接被类名调用,(类名.静态成员);
使用:(一定要提到类加载)
- **静态变量:**修饰成员变量
- 静态变量属于类,被所有的对象所共享,在内存中只有一个副本(静态变量之所以又称为类变量,是因为它和类关联在在一起,随着类加载而存在于方法区中(而不是堆中))
- 静态变量当且仅当在类初次加载时会被初始化
- 静态成员变量可以通过类直接访问
- 非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响
- 非静态成员变量必须要创建实例后才能访问
- **静态方法:**修饰成员方法
- 在静态方法中只能访问静态成员(静态变量或者静态方法),不能访问非静态成员
- 静态方法随着类加载而加载
- 调用方式:类名.静态方法名
- 静态代码块
- static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。
注:
-
独立于类
-
在类加载的时候执行,只执行一次
-
修饰的方法只在调用时执行
方法注意事项:
-
静态的方法只能访问静态的成员;
-
非静态得方法即能访问静态得成员(成员变量,成员方法)又能访问非静态得成员;
-
局部变量不能被static修饰;
-
静态得方法中是不可以定义this、super关键字的,因为静态优先于对象存在,所以静态方法不可以出this;
6. synchronized的用法
三种用法:
- 修饰实例方法
- 修饰静态方法
- 修饰代码块
synchronized的作用:
- 原子性:synchronized保证语句块内操作是原子的
- 可见性:synchronized保证可见性(通过“在执行unlock之前,必须先把此变量同步回主内存”实现)
- 有序性:synchronized保证有序性(通过“一个变量在同一时刻只允许一条线程对其进行lock操作”)
一、四种用法(3种)
synchronized是Java中的关键字,是一种同步锁。
修饰一个类:其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象(所有对象共用一把类锁)- 修饰一个普通方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
- 修饰一个静态方法:其作用的范围是整个方法,作用的对象是这个类的所有对象
- 修饰一个代码块:被修饰的代码块称为同步语句块,其作用范围是大括号{}括起来的代码块,作用的对象是调用这个代码块的对象;
synchronized关键字不能继承。
修饰一个类和修饰一个静态方法,作用的对象都是这个类的所有对象。
修饰一个普通方法和一个代码块,作用的对象是调用这个方法或者代码块的对象。
(1)修饰一个类
在synchronized后面的()里面加入要锁住的类名
class ClassA{
public void funcA() {
synchorized(ClassName.class) {
//TODO
}
}
}
(2)修饰一个普通方法
直接在方法的返回类型之前加synchronized关键字修饰
//1 作用的对象是调用这个方法的对象;
public synchronized void method(){
//TODO
}
//2
public void method(){
synchronized(this) {
// todo
}
写法一修饰的是一个方法,写法二修饰的是一个代码块,但写法一与写法二是等价的,都是锁定了整个方法时的内容。
接口方法不能使用synchronized关键字
构造方法不能使用关键字,但可以使用synchronized代码块进行同步
如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。
//在子类方法中加上synchronized关键字
class Parent {
public synchronized void method() { }
}
class Child extends Parent {
public synchronized void method() { }
}
----
//在子类方法中调用父类的同步方法
class Parent {
public synchronized void method() { }
}
class Child extends Parent {
public void method() { super.method(); }
}
(3)修饰一个静态方法
静态方法时属于类而不是属于对象的,所以synchronized修饰的静态方法锁住的是这个类的所有对象。
//其作用的范围是整个方法,作用的对象是这个类的所有对象;
public synchronized static void func2(){
//TODO
}
(4)修饰一个代码块
一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞
public class ClassB {
//类的实例对象: 同步代码块,锁住的是该类的实例对象
//其作用范围是大括号{}括起来的代码块,作用的对象是调用这个代码块的对象
synchronized(this) {
//TODO
}
//类对象: 同步代码块,锁住的是该类的类对象(该类的所有实例),所有实例对象共用一把类锁
synchronized(ClassB.class) {
//TODO
}
//任意实例对象Object: 同步代码块,锁住的是配置的实例对象
//String对象作为锁
String lock = "";
synchronized(lock) {
//TODO
}
}
二、synchronized底层实现
在Java里面,最基本的互斥同步手段是synchronized
(1)对象
在HotSpot中,对象在堆内存中的存储布局划分为:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
- 对象头:(实现synchronized的锁对象的基础)
- 存储对象自身的运行时数据(如hashCode,GC分代年龄,锁状态标志,线程持有的锁等)-----MarkWord
- 类型指针:对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例(如果对象是数据,还必须在对象头中存储数组大小)
- Java虚拟机可以通过普通Java对象的元数据信息确定Java对象大小,但是如果数组的长度不确定的话,就无法通过元数据中的信息推断出数组的大小
- 实例数据:对象真正存储的有效信息
- **对齐填充:**不是必然存在的,仅仅起着占位符的作用。HotSpot虚拟机中要求所有对象的大小必须是8字节的整数倍。对象头部分已经是8字节的整数倍了,如果对象实例数据部分没有对齐的话,就需要通过对齐方式填充来补全。
(2)原理
- synchronized关键字经过反编译之后,会在同步块的前后分别形成 monitorenter和monitorexit这两个字节码指令。~~这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象。(这句话就不必说了)~~其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置。
- 当代码执行到monitorenter 指令时,将会尝试获取该对象对应的Monitor的所有权,即尝试获得该对象的锁。当该对象的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有该对象monitor的持有权,那它可以重入这个 monitor ,计数器的值也会加 1。
- 与之对应的执行monitorexit指令时,锁的计数器会减1。倘若其他线程已经拥有monitor 的所有权,那么当前线程获取锁失败将被阻塞
并进入到_WaitSet 中,直到等待的锁被释放为止。也就是说,当所有相应的monitorexit指令都被执行,计数器的值减为0,执行线程将释放 monitor(锁),其他线程才有机会持有 monitor 。(当锁计数器为0时,该锁就会被释放)
被synchronized修饰的同步块对同一条线程来说是可重入的。这意味着同一线程反复进入同步块也不会出现自己把自己锁死的情况。
被synchronized修饰的同步块在持有锁的线程执行完毕并释放锁之前,会无条件地阻塞后面其他线程的进入。这意味着无法像处理某些数据库中的锁那样,强制已获取锁的线程释放锁;也无法强制正在等待锁的线程中断等待或超时退出。
**ACC_SYNCHRONIZED标识,**该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
javac----编译.java文件成为字节码文件(class文件)
java-----执行字节码文件(.class文件)
javap—JDK自带的反汇编器,可以查看Java编译器为我们生成的字节码
(4)synchronized的缺点
从执行成本的角度看,持有锁是一个重量级(Heavy-Weight)的操作。在第10章中我们知道了在主流Java虚拟机实现中,Java的线程是映射到操作系统的原生内核线程之上的,如果要阻塞或唤醒一条线程,则需要操作系统来帮忙完成,这就不可避免地陷入用户态到核心态的转换中,进行这种状态转换需要耗费很多的处理器时间。尤其是对于代码特别简单的同步块(譬如被synchronized修饰的getter()或setter()方法),状态转换消耗的时间甚至会比用户代码本身执行的时间还要长。
7. MVP架构
- 对它有什么了解
- 使用它的好处(提到了面向接口编程)
- 面向接口编程会带来什么问题?
8. 数组实现一个ArrayList
9. 数组和链表有什么区别
数组下标越界不同于内存溢出(呜呜呜,我居然答混了)
LinkedList是采用链表的方式来实现List接口的,它本身有自己特定的方法,如: addFirst(),addLast(),getFirst(),removeFirst()等. 由于是采用链表实现的,因此在进行insert和remove动作时在效率上要比ArrayList要好得多!适合用来实现Stack(堆栈)与Queue(队列),前者先进后出,后者是先进先出.
10. 栈和队列有什么区别
栈和队列的区别
- 队列先进先出,栈先进后出
- 对插入和删除操作的"限定"
- 栈是限定只能在表的一端进行插入和删除操作的线性表
- 队列是限定只能在表的一端进行插入和在另一端进行删除操作的线性表。
- 线性表允许在表内任一位置进行插入和删除
- 遍历数据速度不同
- 栈只能从头部取数据,也就最先放入的需要遍历整个栈最后才能取出来,而且在遍历数据的时候还得为数据开辟临时空间,保持数据在遍历前的一致性
- 队列则不同,它基于地址指针进行遍历,而且可以从头或尾部开始遍历,但不能同时遍历,无需开辟临时空间,因为在遍历的过程中不影响数据结构,速度要快的多
要比ArrayList要好得多!适合用来实现Stack(堆栈)与Queue(队列),前者先进后出,后者是先进先出.
10. 栈和队列有什么区别
栈和队列的区别
- 队列先进先出,栈先进后出
- 对插入和删除操作的"限定"
- 栈是限定只能在表的一端进行插入和删除操作的线性表
- 队列是限定只能在表的一端进行插入和在另一端进行删除操作的线性表。
- 线性表允许在表内任一位置进行插入和删除
- 遍历数据速度不同
- 栈只能从头部取数据,也就最先放入的需要遍历整个栈最后才能取出来,而且在遍历数据的时候还得为数据开辟临时空间,保持数据在遍历前的一致性
- 队列则不同,它基于地址指针进行遍历,而且可以从头或尾部开始遍历,但不能同时遍历,无需开辟临时空间,因为在遍历的过程中不影响数据结构,速度要快的多