面试总结

基于鸿洋公众号文章整理答案

java 面试题

java基础

java中==和equals和hashCode的区别

  1. “==”运算符用来比较两个变量的值是否相等.也就是说比较的是变量对应内
    存中所存储的数值是否相同.要比较两个基本类型的数据或者两个引用变量
    是否相等,只能使用”==”运算符
  2. “equals”方法Obeject类中的方法,如果没有覆盖重写”equals”方法的话,”equals”方法会以”==”的方式比较l两个对象的引用是否相等.如果覆盖了”equals”方法即可自定义比较方式.例如String类中覆盖了”equals”方法,其比较的是字符串的序列.改写”equals”方法,必须要遵守通用约定:
    • 自反性: 对于任意的引用值x,x.equals(x)一定为true。
    • 对称性:对于任意的引用值x 和 y,当x.equals(y)返回true时,y.equals(x)也一定返回true。
    • 传递性:对于任意的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也一定返回true。
    • 一致性:对于任意的引用值x 和y,如果用于equals比较的对象信息没有被修改,多次调用x.equals(y)要么一致地返回true,要么一致地返回false。
    • 非空性:对于任意的非空引用值x,x.equals(null)一定返回false。
  3. “hashCode”方法也是Obeject类中的方法,也可用来鉴定两个类是否相等,”hashCode”方法返回该对象的哈希码值,该值通常是一个由对象的内部地址转换的整数,它的实现主要是为了提高哈希表的性能.

    必须铭记: 每个重写了equals的类中,必须要重写hashCode方法.如果不这样的话就违反了Object.hashCode的通用约定,从而导致该类无法与所有基于散列值(hash)的集合类结合在一起正常运行
  4. hashCode()的返回值和equals()的关系如下:

    • equals相等的两个对象, hashCode值一定相等
    • equals不相等的两个对象,hashCode有可能相

int、char、long各占多少字节数

  • 1字节: byte , boolean
  • 2字节: short , char
  • 4字节: int , float
  • 8字节: long , double
  • 注:1字节(byte)=8位(bits)

int与integer的区别

  • integer是int的包装类型
  • integer变量必须实例化后才能使用,而int变量不需要
  • integer实际是对象的引用,当new一个integer时,实际上是生成一个指针指向此对象;而int则直接存储数值
  • integer的默认值是null;int的默认值是0

一些难点

  • 两个通过new生成的integer变量不会相等
  • integer和int比较时,只要两个变量值是相等的则结果为true
1
2
3
4
Integer i = new Integer(100);
int a = 100;
i == a // true
// 因为Integer和int比较时,Java会自动拆包装为int,所以实际上是int与int的比较
  • 非new生成的Integer和new生成的Integer的变量比较时结果为false
1
2
3
4
Integer i = new Integer(100);
Integer j = 100;
i==j // false
//因为j生成的Integer指向的是Java常量池中的对象,而i生成的变量指向堆中的对象,地址值不同,所以不相等
  • 对于两个非new的Integer对象进行比较,如果两个变量的值在-128~127之间结果为true,否则为false
1
2
3
4
5
6
Integer i = 100;
Integer j = 100;
i==j //true
Integer m = 128;
Integer n =128;
m == n //false

谈谈对java多态的理解

多态字面意思即”多种状态”,在面向对象语言中,接口多种不同的实现方式即为多态.引用Charlie Calverts对多态的描述——多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作.同一操作作用于不同的对象可以有不同的解释,产生不同的执行结果.在运行时,可以通过指向基类的指针,来调用派生类中的方法.

多态三要素

Java中实现多态,要有集成,要有重写,父类引用指向子类对象

多态的好处

  • 可替换性: 多态可对已存在的代码具有可替换性.
  • 可扩充性: 增加新的子类不影响已存在类的多态性,继承性以及其他特性的运行和操作
  • 接口性: 多态类是超类通过方法签名,向子类提供一个共同的接口,由子类具体实现和完善
  • 灵活性: 它在应用中体现了灵活多样的操作,提高了使用效率。
  • 简化性: 多态简化对应用软件的代码编写和修改过程

自己理解

我感觉因为有多态的存在,我们在代码编写中,才能将骨架抽取出来,
将相似的类进行归类.因为多态的就可以写出相同相貌.我们可以将子类向
上转型作为父类来做一些统一的操作,比如统一处理错误,统一处理相同的
操作,吐司,弹窗,loading加载等;也可以将父类向下转型到某一具体子类,
去做子类特定的操作.

String,StringBuffer和StringBuilder的区别

  • 在执行速度上StringBuilder > StringBuffer > String
  • String 为字符串常量;StringBuilder与StringBuffer为字符串变量,即String对象一旦创建之后该对象是不可更改的,但后两者是可以更改的.
    具体解释如下:
1
2
3
4
String str = "abc";
System.out.println(str);
str = str + "de";
System.out.println(str);

运行这段代码,运行后先输出”abc”,接着输出”abcde”.看上去str改变了.实际运行过程为: 首先创建一个String对象str,并把“abc”赋值给str,然后在第三行中,其实JVM又创建了一个新的对象也名为str,然后再把原来的str的值和“de”加起来再赋值给新的str,而原来的str就会被JVM的垃圾回收机制(GC)给回收掉了.

而StringBuilder与StringBuffer是变量,对变量进行操作实际上就是对该对象进行修改,而不进行创建回收操作,所以在速度上要比String快很多

而StringBuilder与StringBuffer在方法是完全等价的,但是StringBuilders是线程不安全的;
StringBuffer的方法上带有synchronized关键字,是线程安全的

适用场景

  • String适用于少量的字符串操作
  • StringBuilder 适用于单线程下大量的字符串操作
  • StringBuffer 适用于多线程下的大量字符串操作

什么是内部类,内部类的作用

内部类简单来说就是在一个类中还有一个类.可以分为成员内部类,局部内部类,静态内部类和匿名内部类.

内部类分类

成员内部类
  • 作为外部类的一个成员存在,与外部类的成员,方法并列.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Outer {
private String str1 = "我是外部类";
private int i;
//成员内部类
class Inner{
private String str2 = "我是内部类";
public void test(){
System.out.println(str1);
System.out.println(str2);
}
}
}

Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.test();
//执行结果
我是外部类
我是内部类
  • 特点:
    • 成员内部类可以无条件访问外部类变量;即使是private的.
    • 但是外部类不能直接访问内部类,需要通过创建对象去访问;
    • 要想创建内部类,必须先创建外部类
    • 需要注意的是: 成员内部类不能含有static的变量,代码块和方法,因为成员内部类需要先创建外部类,才能创建自己
局部内部类
  • 定义在方法或者一个作用域中的内部类,他的访问权限仅限于他的作用域.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Outer {
private String str1 = "我是外部类";
public void test(String text) {//jdk1.8不必用final修饰,但是1.7必须用final修饰
class Inner {
public void inTest() {
System.out.println(str1);
System.out.println(text);
}
}
new Inner().inTest();
}
}

Outer outer = new Outer();
outer.test("我是测试数据");
//执行结果
我是外部类
我是测试数据
  • 特点
    • 局部内部类类似局部变量,访问权限仅限于该作用域内
    • 需要注意的是: 局部内部类前不能加private,protected和public修饰,也不能用static修饰;内部类中也不能有static变量
静态内部类
  • 静态内部类定义在类中,任何方法外,用static修饰
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Outer {
private String str1 = "我是外部类";
private static String str2 = "我是静态变量";

static class Inner {
private String str3 = "我是内部类";

public void test() {
// System.out.println(str1); 报错
System.out.println(str2);
System.out.println(str3);
}
}
}
Outer.Inner inner = new Outer.Inner();
inner.test();
//执行结果
我是静态变量
我是内部类
  • 特点
    • 静态内部类不依赖外部类,跟静态变量类似
    • 只能访问外部类的静态变量
    • 静态内部类可以定义静态变量
匿名内部类
  • 匿名内部类就是没有名字的内部类,隐式的集成一个父类或实现某个卡接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class Inner {
public abstract void test();
}
public class Outer {
public void test(Inner inner) {
inner.test();
}
}
public static void main(String[] args) {
Outer outer = new Outer();
outer.test(new Inner() {
@Override
public void test() {
System.out.println("我是匿名内部类");
}
});
}
// 执行结果
我是匿名内部类
  • 特点
    • 匿名内部类不能有构造方法
    • 不能定义任何静态成员和方法
    • 不能用public,protected,private,static修饰
    • 只能创建一个内部类实例
    • 一个匿名内部类一定是在new的后面,用其隐含实现一个j接口或者实现一个类
    • 因匿名内部类为局部内部类,局部类的所有限制都对其生效

内部类作用

  • 摆脱Java集成的局限性,可以用内部类实现多重集成
  • 内部类提供更好的封装

抽象类和接口的区别

  • 从设计角度来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为规范,interface强调特定功能的实现,而abstract class强调所属关系
  • 一个类可以实现多个接口,但是只能继承一个类
  • 抽象类中可以写非抽象方法,避免重复编写,提高代码复用;接口的方法则全是抽象的没有具体实现.jdk1.8 接口中用default修饰的方法可以有函数体
  • 抽象类中的成员变量可以是各种类型;接口的成员变量只能是public static final类型
  • 抽象类可以有静态方法和静态代码块;接口没有

抽象类的意义

抽象类往往用来表征对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。

抽象类可以构造出一个固定的一组行为的抽象描述,但是这组行为却可以有任意的实现方式

自己的理解

抽象类可以通过抽象方法来约束子类的特定行为,使其子类保证对抽象方法的实现;类也可以将公共方法,变量提取封装,避免重复编写代码;

抽象类与接口的应用场景

抽象类的使用场景

概念上的使用场景

在即需要统一的接口,又需要实例变量或缺省方法的情况下就饿可以使用抽象类,具体常见应用如下

  • 定义了一组接口,但是又不强迫每个实现类都必须实现所有接口,可以用抽象类定义一组方法体,甚至可以是空方法体,然后由子类来选择实现
  • 有时候,只靠纯粹的接口不能满足类与类之间功能协调,必须在类中表示状态的变量来区别不同的关系.抽象类的中介关系可以满足这一点
  • 规范了一组互相x协调的方法,其中一些方法是共同的,与状态无关的,类之间共享的,无需子类分别实现;而另一些方法却需子类根据自己的特性来实现具体的功能
自己代码中用到抽象类的地方
  • BaseActivity,BaseFragment等基类,封装了公共方法,但是有些需要特定类实现
  • MyCallback网络请求回调处理类,统一进行了错误处理,正确的流程都具体实现,实现了过滤

接口的应用场景

概念上的使用场景
  • 类与类之间需要特定的接口进行协调,而不在乎具体实现
  • 作为能够实现特定功能标识的存在,也可以是什么都没有的纯粹标识
  • 需要将一个组类视为单一类,而调用者只通过接口和这组类发生联系
  • 需要实现特定的多项功能,而这些功能之间没有特定关系
自己代码中用到抽象类的地方
  • APP采用的是MVP架构,在model和Presenter,View与Presenter之间均需要接口暴露各自的功能
  • 还有一些自定义控件的方法回调也需要接口来实现

抽象类是否可以没有方法和属性?

可以,有抽象方法的类一定是抽象类,但是抽象类不一定有抽象方法

接口的意义

  • 规范性: 接口就是规范,在实际应用系统开发中,涉及到很多层,以及多人协调工作.为了使各个系统之间透明调用,以及多人同时工作就需要接口.我们使用接口的人只需要知道接口是用来做什么的,具体怎么实现不用关心.这样在实际业务中就可以快速开发.
  • 扩展性: 实际开发中,需求是不断变化的,通过接口就不需要频繁更改代码,正好符合对修改关闭,对拓展开放的原则.降低耦合.
  • 提供更好的封装:接口描述对外的功能,而不涉及关系具体的代码实现

泛型中extends和super的区别

源自Java泛型中 extends 和 super 的区别?

<? extends T>和<? super T>是Java泛型中的“通配符(Wildcards)”和“边界(Bounds)”的概念。

  • <? extends T> 是指上界通配符
  • <? super T> 是指下界通配符

为什么要有通配符和边界

在使用泛型中,经常会出现一下情况,我们有Fruit类和它的派生类Apple

1
2
public class Fruit{}
public class Apple extends Fruit{}

然后有一个容器Plate类,盘子里可以放一个泛型的“东西”。我们可以对这个东西做最简单的“放”和“取”的动作:set( )和get( )方法。

1
2
3
4
5
6
public class Palte<T>{
private T item;
public Plate(T t){item=t;}
public void set(T t){item=t;}
public T get(){return item;}
}

现在定义一个水果盘子,逻辑上水果盘子当然可以装苹果

1
2
Plate<Fruit> p = new Plate<Apple>(new Apple())
//抛出异常error: incompatible types: Plate<Apple> cannot be converted to Plate<Fruit>

实际上编译器是这么认为的

  • 苹果 is a 水果
  • 装苹果的盘子 is not a 装水果的盘子

所以,就算容器里装的东西之间有继承关系,但容器之间是没有继承关系的。为了让泛型用起来更舒服,Sun的大脑袋们就想出了<? extends T>和<? super T>的办法,来让“水果盘子”和“苹果盘子”之间发生关系

什么是上界

下面代码就是”上界通配符”

1
2
3
4
5
6
7
Plate<? extends Fruit> 
//简单来说:一个能放水果以及一切水果派生类的盘子Plate<? extends
Fruit>和Plate<Apple>最大的区别就是:Plate<? extends Fruit>是
Plate<Fruit>以及Plate<Apple>的基类。直接的好处就是,我们可以
用“苹果盘子”给“水果盘子”赋值了。

Plate<? extends Fruit> p=new Plate<Apple>(new Apple());

如果把Fruit和Apple的例子再扩展一下,食物分成水果和肉类,水果有苹果和香蕉,肉类有猪肉和牛肉,苹果还有两种青苹果和红苹果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Lev 1
class Food{}

//Lev 2
class Fruit extends Food{}
class Meat extends Food{}

//Lev 3
class Apple extends Fruit{}
class Banana extends Fruit{}
class Pork extends Meat{}
class Beef extends Meat{}

//Lev 4
class RedApple extends Apple{}
class GreenApple extends Apple{}

在这个体系中,下界通配符 Plate<? extends Fruit> 覆盖下图中蓝色的区域。

什么是下界

相对应的,下界通配符

1
Plate<? super Fruit>

表达的是相反的概念:一个能放水果及其一切是水果基类的盘子.Plate<? super Fruit>是Plate的基类,但不是Plate的基类。对应刚才那个例子,Plate<? super Fruit>覆盖下图中红色的区域。

上下界通配符的副作用

边界让Java不同泛型之间的转换更容易了。但不要忘记,这样的转换也有一定的副作用。那就是容器的部分功能可能失效。

还是以刚才的Plate为例。我们可以对盘子做两件事,往盘子里set()新东西,以及从盘子里get()东西。

1
2
3
4
5
6
class Plate<T>{
private T item;
public Plate(T t){item=t;}
public void set(T t){item=t;}
public T get(){return item;}
}
  • 上界<? extends T>不能往里存,只能往外取
    上届<? extends Fruit> 会使往盘子里set()方法失效.但是取东西get()有效,例子如下:
1
2
3
4
5
6
7
8
9
10
Plate<? extends Fruit> p=new Plate<Apple>(new Apple());

//不能存入任何元素
p.set(new Fruit()); //Error
p.set(new Apple()); //Error

//读取出来的东西只能存放在Fruit或它的基类里。
Fruit newFruit1=p.get();
Object newFruit2=p.get();
Apple newFruit3=p.get(); //Error

原因是编译器只知道容器内是Fruit和其子类,但具体是什么并不知道.可能是Fruit?可能是Apple?也可能是Banana,RedApple,GreenApple?编译器在看到后面用Plate赋值以后,盘子里没有被标上有“苹果”。而是标上一个占位符:CAP#1,来表示捕获一个Fruit或Fruit的子类,具体是什么类不知道,代号CAP#1。然后无论是想往里插入Apple或者Meat或者Fruit编译器都不知道能不能和这个CAP#1匹配,所以就都不允许。

所以通配符<?>和类型参数的区别就在于,对编译器来说所有的T都代表同一种类型。比如下面这个泛型方法里,三个T都指代同一个类型,要么都是String,要么都是Integer。

1
public <T> List<T> fill(T... t);

但通配符<?>没有这种约束,Plate<?>单纯的就表示:盘子里放了一个东西,是什么我不知道。所以题主问题里的错误就在这里,Plate<? extends Fruit>里什么都放不进去。

  • 下界<? super T>不影响往里存,但往外取只能放在Object对象里

使用下界<? super Fruit>会使从盘子里取东西的get( )方法部分失效,只能存放到Object对象里。set( )方法正常。

1
2
3
4
5
6
7
8
9
10
Plate<? super Fruit> p=new Plate<Fruit>(new Fruit());

//存入元素正常
p.set(new Fruit());
p.set(new Apple());

//读取出来的东西只能存放在Object类里。
Apple newFruit3=p.get(); //Error
Fruit newFruit1=p.get(); //Error
Object newFruit2=p.get();

因为下界规定了元素的最小粒度的下限,实际上是放松了容器元素的类型控制。既然元素是Fruit的基类,那往里存粒度比Fruit小的都可以。但往外读取元素就费劲了,只有所有类的基类Object对象才能装下。但这样的话,元素的类型信息就全部丢失。

PECS原则

最后看一下什么是PECS(Producer Extends Consumer Super)原则,已经很好理解了:

  • 频繁往外读取内容的,适合用上界Extends。
  • 经常往里插入的,适合用下界Super。

父类的静态方法能否被子类重写

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
	public class Father {

public static void hello() {
System.out.println("father.......hello.......");
}

public void method() {
System.out.println("father.........method......");
}
}


public class Son extends Father {
public static void hello() {
System.out.println("Son.......hello.......");
}

@Override
public void method() {
System.out.println("Son.........method......");
}

public static void main(String[] args) {
Father father = new Son();
father.method();
// father.hello();//error 不能通过实例访问静态成员
Father.hello();
Son.hello();
}
}
//执行结果
Son.........method......
father.......hello.......
Son.......hello.......
// 版本2
public class Son extends Father {
@Override
public void method() {
System.out.println("Son.........method......");
}

public static void main(String[] args) {
Father.hello();
Son.hello();
}
}
//执行结果
father.......hello.......
father.......hello.......

结论:

  • 子类不能重写父类的静态方法
  • 子类可以继承父类的静态方法,在父类此方法对子类可见时,子类可以调用
  • “重写”只能适用于实例方法.不能用于静态方法.对于静态方法,只能隐藏(形式上被重写了,但是不符合的多态的特性),“重写”是用来实现多态性的,只有实例方法是可以实现多态,而静态方法无法实现多态。

final,finally,finalize的区别

  • final 是修饰关键字,用于修饰类、成员变量和成员方法.

    • 修饰方法时,该方法可以被继承但是不能覆盖,个人理解为方法在使用final修饰之后就相当与被固定了,所以可以被子类引用,但是不能覆盖;
    • 修饰类时,被修饰的类不能被继承,也就是说在当前类在后期的引用中,不需要扩展,实现过程不需要改变,就可以将该类用final来修饰。
  • finally 作为异常处理的一部分,它只能用在try/catch语句中,并附带一个语句块,并且附带一个语句块,表示这段语句最终一定会被执行(不管有没有抛出异常),经常被用在需要释放资源的情况下。

  • finalize 是Java提供方法,回收空间; 在gc回收内存之前,会先执行finalize方法;gc回收的是new出来的空间,其他的空间则需要finalize()来回收;一个对象的finalize方法只会被执行一次,但是在调用之后该对象不一定会被gc立刻收回;在finalize中的异常会被忽略;

Serializable 和Parcelable 的区别

  • Serializable

    • Serializable 是Java提供的序列化方式
    • Serializable使用IO读写存储在硬盘上.序列化的过程使用了反射技术,并且期间会产生大量的临时变量,引起频繁GC.
    • 优点是代码量少,只需要实现接口即可实现
  • Parcelable

    • Parcelable 是android特有的序列化方式,实在内存中进行序列化
    • Parcelable的实现原理是将一个完整对象进行分解,而分解后的每一部分都是Intent支持的类型,实现起来较为复杂
    • 优点是效率高

java深入源码级的面试题

哪些情况下的对象会被垃圾回收机制处理掉?

讲一下常见编码方式?