目录

Java后端面试题整理

1、面向对象

什么是面向对象?

对比面向过程,是两种不同的处理问题的角度

面向过程更注重事情的每一个步骤及顺序,面向对象更注重事情有哪些参与者(对象)、及各自需要做什么

比如:洗衣机洗衣服

面向过程

面向过程会将任务拆解成一系列的步骤(函数),1、打开洗衣机—>2、放衣服–>3、放洗衣粉—>4、清洗—–>5、烘干 面向对象会拆出人和洗衣机两个对象:

人:打开洗衣机放衣服放洗衣粉洗衣机:清洗烘干

从以上例子能看出,面向过程比较直接高效,而面向对象更易于复用、扩展和维护

面向对象

封装

封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项内部细节对外部调用透明,外部调用无需修改或者关心内部实现

两个经典的场景

1、javabean举例:

javabean的属性私有,提供getset对外访问,因为属性的赋值或者获取逻辑只能由javabean本身决定。而不能由外部胡乱修改

1
2
3
4
private string name ;
public void setName(string name){
		this.name = "tuling_"+name;
	}

该name有自己的命名规则,明显不能由外部直接赋值

2、orm框架举例

操作数据库,我们不需要关心链接是如何建立的、sql是如何执行的,只需要引入mybatis,调方法即可

继承

继承基类的方法,并做出自己的改变和/或扩展

子类共性的方法或者属性直接使用父类的,而不需要自己再定义,只需扩展自己个性化的

多态

基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。

继承,方法重写,父类引用指向子类对象

1
2
父类类型变量名=new子类对象;
变量名.方法名();

无法调用子类特有的功能

2、JVM性能调优实战相关

./2.jpg

3、JDK JRE JVM

JDK

Java Develpment Kit java开发工具

JRE

Java Runtime Environment java运行时环境

JVM:

java Virtual Machine java虚拟机

./1.jpg

4、==equals

==和equals比较==对比的是栈中的值,基本数据类型是变量值,引用类型是堆中内存对象的地址

equals: object中默认也是采用==比较,通常会重写

Object

1
2
3
pub1ic boolean equals(Object obj) {
	return (this == obj);
}

String

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
pub1ic boolean equals (Object anObject) {
    if(this ==anobject) {
		return true;
	}
    if (anObject instanceof String) i
    	String anotherstring = (String)anobject;
    	int n = value.length;
        if (n == anotherstring.value.length) {
            char v1[] = value;
            char v2[] = anotherstring. value;int i = o;
            while (n-- != 0) {
            if (v1[i] != v2[i])
                return false;
                i++;
            }
            return true;
    	}
    }
    return false;
}

上述代码可以看出,String类中被复写的equals()方法其实是比较两个字符串的内容。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class StringDemo {
	public static void main(string args[]) {
		String str1 = "He11o";
		String str2 new string ( "He11o");
		String str3 = str2; //引用传递
		System.out.println(str1 == str2); //false
		System.out.println(str1 == str3); // false
		System.out.println(str2 == str3); // true
		System.out.println(str1.equals (str2));// true
		System.out.println(str1.equals (str3));//true
		System.out.println(str2.equals (str3)); // true
    }
}

5、final

1、简述final作用

最终的

  • 修饰类:表示类不可被继承
  • 修饰方法:表示方法不可被子类覆盖,但是可以重载
  • 修饰变量:表示变量一旦被赋值就不可以更改它的值。

(1)修饰成员变量

  • 如果final修饰的是类变量,只能在静态初始化块中指定初始值或者声明该类变量时指定初始值。
  • 如果final修饰的是成员变量,可以在非静态初始化块、声明该变量或者构造器中执行初始值。

(2)修饰局部变量

系统不会为局部变量进行初始化,局部变量必须由程序员显示初始化。 因此使用final修饰局部变量时,即可以在定义时指定默认值(后面的代码不能对变量再赋值),也可以不指定默认值,而在后面的代码中对final变量赋初值(仅一次)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
pub1ic class Fina1var {
    final static int a = 0;//再声明的时候就需要赋值或者静态代码块赋值
    /**
    static{
    	a =o;
    }
    */
    final int b = 0;//再声明的时候就需要赋值或者代码块中赋值或者构造器赋值
    /*{
    	b = 0;
    }
    */
    public static void main(string[]args) {
        final int localA;//局部变量只声明没有初始化,不会报错,与fina1无关。
        loca1A = 0;//在使用之前一定要赋值I
        //localA = 1;但是不允许第二次赋值
    }
}

(3)修饰基本类型数据和引用类型数据

  • 如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;
  • 如果是引用类型的变量,则在对某初始化之后便不能再让其指向另一个对象。但是引用的值是可变的。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
pub1ic class Fina1ReferenceTest{
    public static void main(){
        final int[] iArr={1,2,3,4};
        iArr[2]=-3;//合法
        iArr=nu1;//非法,对iArr不能重新赋值
        final Person p = new Person(25);
        p.setAge(24);//合法
        p=null;//非法
    }
}

为什么局部内部类和匿名内部类只能访问局部final变量?

编译之后会生成两个class文件,Test.class Test1.class

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Test {
    public static void main(string[] args){}
    //局部fina1变量a,b
    public void test(final int b) {
        final int a = 10;
        //匿名内部类
        new Thread(){
            pub1ic void run() i
                System.out.print1n(a); 
                System.out.print1n(b); 
            	};
            }.start();
    }
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
c1ass outc1ass {
    private int age = 12;
    public void outPrint(final int x) {
        c1ass Inclass i
            pub1ic void InPrint() {
                System. out.println(x);
                System.out.println(age);
            }
        }
    new Inc1ass(().InPrint(;
    }
}

2、为什么局部内部类和匿名内部类只能访问局部final变量?

首先需要知道的一点是:内部类和外部类是处于同一个级别的,内部类不会因为定义在方法中就会随着方法的执行完毕就被销毁。

这里就会产生问题:当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象可能还存在(只有没有人再引用它时,才会死亡)。这里就出现了一个矛盾:内部类对象访问了一个不存在的变量。为了解决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以访问它,实际访问的是局部变量的"copy"。这样就好像延长了局部变量的生命周期

将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的,也就是如果我们在内部类中修改了成员变量,方法中的局部变量也得跟着改变,怎么解决问题呢?

就将局部变量设置为final,对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量和方法的局部变量的一致性。这实际上也是一种妥协。使得局部变量与内部类内建立的拷贝保持一致。

11、ArrayList和LinkedList区别

ArrayList:

基于动态数组,连续内存存储,适合下标访问(随机访问),扩容机制:因为数组长度固定,超出长度存数据时需要新建数组,然后将老数组的数据拷贝到新数组,如果不是尾部插入数据还会涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能、甚至超过linkedList(需要创建大量的node对象)

LinkedList:

基于链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询:需要逐—遍历 遍历LinkedList必须使用iterator不能使用for循环,因为每次for循环体内通过get(i)取得某一元素时都需要对list重新进行遍历,性能消耗极大。 另外不要试图使用indexof等返回元素索引,并利用其进行遍历,使用indexlOf对list进行了遍历,当结果为空时会遍历整个列表。

6、Stringbuilfer String、StringBuffer、StringBuilder

String是final修饰的,不可变,每次操作都会产生新的String对象

StringBuffer和stringBuilder都是在原对象上操作

StringBuffer是线程安全的,StringBuilder线程不安全的

StringBuffer方法都是synchronized修饰的

性能: StringBuilder > StringBuffer > String

场景:经常需要改变字符串内容时使用后面两个

优先使用StringBuilder,多线程使用共享变量时使用StringBuffer

7、重载和重写的区别

重载

发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。

重写

发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方法。

1
2
public int add(int a,String b)
public string add(int a,String b)//编译报错,产生二义性,并不知道返回什么值

8、接口和抽象类的区别

  • 抽象类可以存在普通成员函数,而接口中只能存在public abstract方法。
  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的。(java8之前,java9变了)
  • 抽象类只能继承一个,接口可以实现多个。

接口的设计目的,是对类的行为进行约束(更准确的说是一种“有"约束,因为接口不能规定类不可以有什么行为),也就是提供一种机制,可以强制要求不同的类具有相同的行为。它只约束了行为的有无,但不对如何实现行为进行限制。

而抽象类的设计目的,是代码复用。当不同的类具有某些相同的行为(记为行为集合A),且其中一部分行为的实现方式一致时(A的非真子集,记为B),可以让这些类都派生于一个抽象类。在这个抽象类中实现了B,避免让所有的子类来实现B,这就达到了代码复用的目的。而A减B的部分,留给各个子类自己实现。正是因为A-B在这里没有实现,所以抽象类不允许实例化出来(否则当调用到A-B时,无法执行)。

抽象类是对类本质的抽象,表达的是is a 的关系,比如: BMw is a car。抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现。

而接口是对行为的抽象,表达的是like a的关系。比如: Bird like a Aircraft(像飞行器一样可以飞),但其本质上is a Bird。接口的核心是定义行为,即实现类可以做什么,至于实现类主体是谁、是如何实现的,接口并不关心。

使用场景:当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。

抽象类的功能要远超过接口,但是,定义抽象类的代价高。因为高级语言来说(从实际设计上来说也是)每个类只能继承一个类。在这个类中,你必须继承或编写出其所有子类的所有共性。虽然接口在功能上会弱化许多,但是它只是针对一个动作的描述。而且你可以在一个类中同时实现多个接口。在设计阶段会降低难度。

9、List和Set的区别

  • List:有序,按对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用lterator取出所有元素,在逐一遍历,还可以使用get(int index)获取指定下表的元素
  • Set:无序,不可重复,最多允许有一个Nul元素对象,取元素时只能用lterator接口取得所有元素,在逐一遍历各个元素

10、hashCode与equals

hashCode介绍

hashCode()的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()定义在JDK的Object.java中,Java中的任何类都包含有hashCode()函数。散列表存储的是键值对(key-value),它的特点是:能根据"键"快速的检索出对应的“值"。这其中就利用到了散列码!(可以快速找到所需要的对象)

为什么要有hashCode

以"Hashset如何检查重复"为例子来说明为什么要有hashcode:

对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,看该位置是否有值,如果没有、HashSet会假设对象没有重复出现。但是如果发现有值,这时会调用equals ()方法来检查两个对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样就大大减少了equals的次数,相应就大大提高了执行速度。

  • 如果两个对象相等,则hashcode—定也是相同的
  • 两个对象相等,对两个对象分别调用equals方法都返回true
  • 两个对象有相同的hashcode值,它们也不一定是相等的·因此,equals方法被覆盖过,则hashCode方法也必须被覆盖
  • 因此,equals方法被覆盖过,则hashCode方法也必须被覆盖
  • hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如 何都不会相等(即使这两个对象指向相同的数据)

11、ArrayList和LinkedList区别

ArrayList:基于动态数组,连续内存存储,适合下标访问(随机访问),扩容机制:因为数组长度固定,超出长度存数据时需要新建数组,然后将老数组的数据拷贝到新数组,如果不是尾部插入数据还会涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能、甚至超过linkedList(需要创建大量的node对象)

LinkedList:基于链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询:需要逐一遍历遍历LinkedList必须使用iterator不能使用for循环,因为每次for循环体内通过geti)取得某一元素时都需要对list重新进行遍历,性能消耗极大。

另外不要试图使用indexOf等返回元素索引,并利用其进行遍历,使用indexlOf对list进行了遍历,当结果为空时会遍历整个列表。

12、HashMap和HashTable的区别?底层实现是什么?

1.区别:

(1) HashMap方法没有synchronized修饰,线程非安全,HashTable线程安全;

(2)HashMap允许key和value为null,而HashTable不允许

2.底层实现:数组+链表实现

  • jdk8开始链表高度到8、数组长度超过64,链表转变为红黑树,元素以内部类Node节点存在
  • 计算key的hash值,二次hash然后对数组长度取模,对应到数组下标,
  • 如果没有产生hash冲突(下标位置没有元素),则直接创建Node存入数组,
  • 如果产生hash冲突,先进行equal比较,相同则取代该元素,不同则判断链表高度插入链表,链表高度达到8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树转回链表
  • key为null,存在下标0的位置

数组扩容

13、ConcurrentHashMap原理,jdk7和jdk8版本的区别

jdk7:

数据结构:ReentrantLock+Segment+HashEntry,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构

元素查询:二次hash,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部

锁: Segment分段锁Segment继承了ReentrantLock,锁定操作的Segment,其他的Segment不受影响,并发度 为segment个数,可以通过构造函数指定,数组扩容不会影响其他的segment

get方法无需加锁,volatile保证(避免脏数据)

jdk8:

数据结构: synchronized+CAS+Node+红黑树,Node的val和next都用volatile修饰,保证可见性

查找,替换,赋值操作都使用CAS

锁:锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容

读操作无锁:

Node的val和next使用volatile修饰,读写线程对该变量互相可见

数组用volatile修饰,保证扩容时被读线程感知

14、如何实现一个IOC容器

  1. 配置文件配置包扫描路径
  2. 递归包扫描获取.class文件
  3. 反射、确定需要交给IOC管理的类
  4. 对需要注入的类进行依赖注入
  • 配置文件中指定需要扫描的包路径

  • 定义一些注解,分别表示访问控制层、业务服务层、数据持久层、 依赖注入注解、获取配置文件注解

  • 从配置文件中获取需要扫描的包路径,获取到当前路径下的文件信息及文件夹信息,我们将当前路径下所有以.class结尾的文件添加到一个Set集合中进行存储

  • 遍历这个set集合,获取在类上有指定注解的类,并将其交给IOC容器,定义一个安全的Map用来存储这些对象

  • 遍历这个lOC容器,获取到每一个类的实例,判断里面是有有依赖其他的类的实例,然后进行递归注入

15、什么是字节码?采用字节码的好处是什么?

java中的编译器和解释器:

Java中引入了虚拟机的概念,即在机器和编译程序之间加入了 -层抽象的虚拟的机器。这台虚拟的机器在任何平台 上都提供给编译程序一个的共同的接口。

编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(即扩展名为.class的文件),它不面向任何特定的处理器,只面向虚拟机。

每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。这也就是解释了Java的编译与解释并存的特点。

Java源代码—>编译器—>jvm可执行的Java字节码(即虚拟指令)—>jvm—>jvm中解释器—->机器可执行的二进制机器码—->程序运行。

采用字节码的好处:

Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。

34、谈谈你对AOP的理解

系统是由许多不同的组件所组成的,每一个组件各负责一块特定功能。除了实现自身核心功能之外,这些组件还经常承担着额外的职责。例如日志、事务管理和安全这样的核心服务经常融入到自身具有核心业务逻辑的组件中去。这些系统服务经常被称为横切关注点,因为它们会跨越系统的多个组件。

当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。

日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用

AOP:将程序中的交叉业务逻辑(比如安全,日志,事务等),封装成一个切面,然后注入到目标对象(具体业务逻辑)中去。AOP可以对某个对象或某些对象的功能进行增强,比如对象中的方法进行增强,可以在执行某个方法之前额外的做一些事情,在某个方法执行之后额外的做一些事情

35、谈谈你对IOC的理解

容器概念、控制反转、依赖注入

ioc容器:实际上就是个map (key,value),里面存的是各种对象(在xml里配置的bean节点、@repository.@service、@controller、@component),在项目启动的时候会读取配置文件里面的bean节点,根据全限定类名使用反射创建对象放到map里、扫描到打上上述注解的类还是通过反射创建对象放到map里。

这个时候map里就有各种对象了,接下来我们在代码里需要用到里面的对象时,再通过DI注入(autowired.resource等注解,xml里bean节点内的ref属性,项目启动的时候会读取xml节点ref属性根据id注入,也会扫描这些注解,根据类型或id注入; id就是对象名)。

控制反转:

没有引入IOC容器之前,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。

引入IOC容器之后,对象A与对象B之间失去了直接联系,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。

通过前后的对比,不难看出来:对象A获得依赖对象B的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是“控制反转"这个名称的由来。

全部对象的控制权全部上缴给"第三方"“OC容器,所以,IOC容器成了整个系统的关键核心,它起到了一种类似"粘合剂的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个粘合剂”,对象与对象之间会彼此失去联系,这就是有人把lOC容器比喻成′粘合剂"的由来。

依赖注入:

“获得依赖对象的过程被反转了"。控制被反转之后,获得依赖对象的过程由自身管理变为了由IOC容器主动注入。依赖注入是实现IOC的方法,就是由IOC容器在运行期间,动态地将某种依赖关系注入到对象之中。

66、简述MylSAM和InnoDB的区别

MyISAM:

不支持事务,现在支持了,但是每次查询都是原子的;支持表级锁,即每次操作是对整个表加锁;存储表的总行数;

一个MyISAM表有三个文件:索引文件、表结构文件、数据文件;

采用非聚集索引,索引文件的数据域存储指向数据文件的指针。辅索引与主索引基本一致,但是辅索引不用保证唯—性。

lnnoDB:

支持ACID的事务,支持事务的四种隔离级别;支持行级锁及外键约束:因此可以支持写并发;不存储总行数;

一个InnoDB引擎存储在一个文件空间(共享表空间,表大小不受操作系统控制,一个表可能分布在多个文件里),也有可能

为多个(设置为独立表空,表大小受操作系统文件大小限制,一般为2G),受操作系统文件大小的限制;

88、Spring Cloud和Dubbo的区别

底层协议: springcloud基于http协议,dubbo基于Tcp协议,决定了dubbo的性能相对会比较好

注册中心: Spring Cloud 使用的eureka , dubbo推荐使用zookeeper

模型定义: dubbo将一个接口定义为一个服务,SpringCloud则是将一个应用定义为一个服务

SpringCloud是一个生态,而Dubbo是SpringCloud生态中关于服务调用一种解决方案(服务治理)

ACID靠什么保证的?

  • A原子性由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql
  • C一致性由其他三大特性保证、程序代码要保证业务上的一致性
  • I隔离性由MVCC来保证
  • D持久性由内存+redo log来保证,Mysq|修改数据同时在内存和redo log记录这次操作,宕机的时候可以从redolog恢复

InnoDB redo log写盘,InnoDB事务进入prepare状态。

如果前面prepare成功,bin1og 写盘,再继续将事务日志持久化到 binlog,如果持久化成功,那么、InnoDB事务则进入commit状态(在 redo log 里面写一个commit记录)

redolog的刷盘会在系统空闲时进行

end