(一) java基础面试知识点
1、java中==,equals和hashCode的区别
“==”:
==是运算符,用来比较两个值、两个对象的内存地址是否相等
“equals()”:
equals是Object类的方法,默认情况下比较两个对象是否是同一个对象,内部实现是通过“==”来实现的。
如果想比较两个对象的其他内容,则可以通过重写equals方法
“hashCode()”:
hashCoed也是Object类里面的方法,返回值是一个对象的哈希码,同一个对象哈希码一定相等,但不同对象哈希码也有可能相等。
如果两个对象通过equals方法比较相等,那么他的hashCode一定相等;
如果两个对象通过equals方法比较不相等,那么他的hashCode有可能相等
hashCode()效率比equals()效率高
HashSet判断元素是否相等时先用hashCode()判断,如果hashCode()不同,则对象不等,如果hashCode()相同,再比较equals() ,大大提高了效率。所以要保证如果重写了equals(),也要重写hashCode()
注:当输入数据量太大,哈希值长度却是固定32位,这意味着哈希值是一个有限集合,无法建立一对一关系,不同对象的hashcode相等是有可能会发生的
2、Java基本类型占用的字节数
1字节: byte , boolean
2字节: short , char
4字节: int , float
8字节: long , double
3、int与integer的区别
Integer是int的包装类,int则是java的一种基本的数据类型;
Integer变量必须实例化之后才能使用,而int变量不需要实例化;
Integer实际是对象的引用,当new一个Integer时,实际上生成一个指针指向对象,而int则直接存储数值;
Integer的默认值是null,而int的默认值是0。
4、谈谈对java多态的理解
多态是指不同子类型的对象对同一行为作出不同的响应;
多态性分为编译时的多态性和运行时的多态性。方法重载实现的是编译时的多态性,而方法重写实现的是运行时的多态性;
对于运行时多态,特别注意,父类引用指向子类对象,在调用实例方法时,调用的是子类重写之后的,并且不能调用子类新增的方法,对于属性和static方法来说,还是执行父类原有的
5、String、StringBuffer、StringBuilder区别
String:String的值是不可变的,这就导致每次对String的操作都会生成新的String对象,不仅效率低下,而且浪费大量优先的内存空间,适用于少量的字符串操作的情况;
StringBuffer:StringBuffer是可变类,且是线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量,适用多线程下在字符缓冲区进行大量操作的情况;
StringBuilder:StringBuilder是可变类,且是线程不安全的字符串操作类,速度更快,适用于单线程下在字符缓冲区进行大量操作的情况
6、什么是内部类?内部类的作用
定义:
将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类
分类:
(1)成员内部类
成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员)。当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员
(2)局部内部类
局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内
(3)匿名内部类
匿名内部类就是没有名字的内部类
(4)静态内部类
指被声明为static的内部类,他可以不依赖内部类而实例,而通常的内部类需要实例化外部类,从而实例化。静态内部类不可以有与外部类相同的类名。不能访问外部类的普通成员变量,但是可以访问静态成员变量和静态方法(包括私有类型)。一个静态内部类去掉static 就是成员内部类,他可以自由的引用外部类的属性和方法,无论是静态还是非静态。但是不可以有静态属性和方法
作用:
(1)每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整;
(2)方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏;
(3)方便编写事件驱动程序;
(4)方便编写线程代码
7、抽象类和接口区别
抽象类:
(1)抽象类使用abstract修饰;
(2)抽象类不能实例化,即不能使用new关键字来实例化对象;
(3)含有抽象方法(使用abstract关键字修饰的方法)的类是抽象类,必须使用abstract关键字修饰;
(4)抽象类可以含有抽象方法,也可以不包含抽象方法,抽象类中可以有具体的方法;
(5)如果一个子类实现了父类(抽象类)的所有抽象方法,那么该子类可以不必是抽象类,否则就是抽象类;
(6)抽象类中的抽象方法只有方法体,没有具体实现
接口:
(1)接口使用interface修饰;
(2)接口不能被实例化;
(3)一个类只能继承一个类,但是可以实现多个接口;
(4)接口中方法均为抽象方法;
(5)接口中不能包含实例域或静态方法(静态方法必须实现,接口中方法是抽象方法,不能实现)
8、抽象类的意义
(1)为子类提供一个公共的类型;
(2)封装子类中重复内容(成员变量和方法);
(3)定义有抽象方法,子类虽然有不同的实现,但该方法的定义是一致的。
9、接口的意义
(1)定义接口的重要性:在Java编程中,abstract class和interface是支持抽象类定义的两种机制。正是由于这两种机制的存在,才使得Java成为面向对象的编程语言;
(2)定义接口有利于代码的规范:对于一个大型项目而言,架构师往往会对一些主要的接口来进行定义,或者清理一些没有必要的接口。这样做的目的一方面是为了给开发人员一个清晰的指示,告诉他们哪些业务需要实现;同时也能防止由于开发人员随意命名而导致的命名不清晰和代码混乱,影响开发效率
(3)有利于对代码进行维护:比如你要做一个画板程序,其中里面有一个面板类,主要负责绘画功能,然后定义了这个类。需求改变时,现有的类已经不能够满足需要,然后需要要重新设计这个类,更糟糕的是可能要放弃这个类,那么其他地方可能有引用他,这样修改起来很麻烦。如果一开始定义一个接口,把绘制功能放在接口里,然后定义类时实现这个接口,那么只需要用这个接口去引用实现它的类就行了,以后要换的话只不过是引用另一个类而已,这样就达到维护、拓展的方便性;
(4)保证代码的安全和严密:一个好的程序一定符合高内聚低耦合的特征,那么实现低耦合,定义接口是一个很好的方法,能够让系统的功能较好地实现,而不涉及任何具体的实现细节。这样就比较安全、严密一些,这一思想一般在软件开发中较为常见
10、抽象类与接口的应用场景
抽象类(abstract class)的应用场合:
(1)定义了一组接口,但又不想强迫每个实现类都必须实现所有的接口。可以用abstract class定义一组方法体,甚至可以是空方法体,然后由子类选择自己所感兴趣的方法来覆盖;
(2)某些场合下,只靠纯粹的接口不能满足类与类之间的协调,还必需类中表示状态的变量来区别不同的关系。abstract的中介作用可以很好地满足这一点;
(3)规范了一组相互协调的方法,其中一些方法是共同的,与状态无关的,可以共享的,无需子类分别实现;而另一些方法却需要各个子类根据自己特定的状态来实现特定的功能
接口(interface)的应用场合:
(1)类与类之前需要特定的接口进行协调,而不在乎其如何实现;
(2)作为能够实现特定功能的标识存在,也可以是什么接口方法都没有的纯粹标识;
(3)需要将一组类视为单一的类,而调用者只通过接口来与这组类发生联系;
(4)需要实现特定的多项功能,而这些功能之间可能完全没有任何联系
11、泛型中extends和super的区别
<? extends T>代表的是上界通配符。只能用于方法返回,告诉编译器此返参类型的最小继承边界为T,T和T的父类都能接收,但是入参类型无法确定,只能接受null的传入
<? super T>代表的是下界通配符,参数类型必须是T或T的子类型,只能用于限定方法入参,而返参只能用Object类接收
12、进程和线程的区别
(1)进程是资源分配的最小单位,线程是程序执行的最小单位(资源调度的最小单位);
(2)进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多;
(3)线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行;
(4)多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间
13、final,finally,finalize的区别
(1)final用于声明属性、方法和类,分别表示属性不可变、方法不可覆盖、类不可被继承(不能再派生出新的子类)
(2)finally作为异常处理的一部分,它只能用在try/catch语句中,并且附带着一个语句块,表示这段语句最终一定被执行,经常被用在需要释放资源的情况下
(3)finalize是Object类中的一个方法,在垃圾收集器执行的时候会调用被回收对象的finalize()方法,可以覆盖此方法来实现对其他资源的回收,例如关闭文件等。需要注意的是,一旦垃圾回收器准备好释放对象占用的空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存
14、序列化的方式
(1)通过实现Serializable接口,这种是隐式序列化(不需要手动),这种是最简单的序列化方式,会自动序列化所有非static和 transient关键字修饰的成员变量;
(2)Externalizable接口继承自Serializable, 我们在实现该接口时,必须实现writeExternal()和readExternal()方法,而且只能通过手动进行序列化,并且两个方法是自动调用的,因此,这个序列化过程是可控的,可以自己选择哪些部分序列化;
(3)先实现Serializable接口,并且添加writeObject()和readObject()方法。注意这里是添加,不是重写或者覆盖。但是添加的这两个方法必须有相应的格式
- 方法必须要被private修饰
- 第一行调用默认的defaultRead/WriteObject()
- 调用read/writeObject()将获得的值赋给相应的值
注:readObject()和writeObject() 既不存在于java.lang.Object,也没有在Serializable中声明,ObjectOutputStream使用了反射来寻找是否声明了这两个方法。因为ObjectOutputStream使用getPrivateMethod,所以这些方法不得不被声明为private以至于供ObjectOutputStream来使用
15、Serializable 和Parcelable 的区别
作用:
Serializable使用了反射技术,并且期间产生临时对象,Parcelable的数据仅在内存中存在,是通过IBinder通信的消息的载体
效率及选择:
Parcelable的性能比Serializable好,在内存开销方面较小,所以在内存间数据传输时推荐使用Parcelable,如activity间传输数据,而Serializable可将数据持久化方便保存,所以在需要保存或网络传输数据时选择Serializable,因为android不同版本Parcelable可能不同,所以不推荐使用Parcelable进行数据持久化
高级功能上:
Serializable序列化不保存静态变量,可以使用transient关键字对部分字段不进行序列化,也可以覆盖writeObject、readObject方法以实现序列化过程自定义。如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。换句话说,用transient关键字标记的成员变量不参与序列化过程
编程实现:
对于Serializable,类只需要实现Serializable接口,并提供一个序列化版本id(serialVersionUID)即可
而Parcelable则需要实现writeToParcel、describeContents函数以及静态的CREATOR变量(AS有相关插件 一键生成所需方法),实际上就是将如何打包和解包的工作自己来定义,而序列化的这些操作完全由底层实现
16、静态属性和静态方法是否可以被继承?是否可以被重写?以及原因
父类的静态属性和方法可以被子类继承
不可以被重写,当父类的引用指向子类时,使用对象调用静态方法或者静态变量,是调用的父类中的方法或者变量,并没有被子类改写
原因:
因为静态方法从程序开始运行后就已经分配了内存,也就是说已经写死了。所有引用到该方法的对象(父类的对象也好子类的对象也好)所指向的都是同一块内存中的数据,也就是该静态方法
17、Sstring 转换成Integer的方式及原理
方式:
调用Integer类的parseInt()方法
原理:
(1)参数校验,包括字符串和进制;字符串不为null,进制在[2,36]之间;
(2)字符串长度不为0,即不为空;否则抛出异常;
(3)取字符串第一位字符,根据字符的ASCII码与‘0’的ASCII码比较判断是否是’+'或者‘-’;
(4)确定正负数后,逐位获得每位字符的int值;
(5)通过*=和-=对各结果进行拼接
(二) java深入源码级的面试题(有难度)
1、常见编码方式
ASCII编码:
用来表示英文,它使用1个字节表示,其中第一位规定为0,其他7位存储数据,一共可以表示128个字符
扩展ASCII码:
用于表示更多的欧洲文字,用8个位存储数据,一共可以表示256个字符
GBK/GB2312/GB18030:
表示汉字,GBK/GB2312表示简体中文,GB18030表示繁体中文
Unicode编码:
包含世界上所有的字符,是一个字符集
UTF-8:
是Unicode字符的实现方式之一,它使用1-4个字符表示一个符号,根据不同的符号而变化字节长度
2、utf-8编码字符占用几个字节
数字占1个字节,英文字母占1个字节,少数是汉字每个占用3个字节,多数占用4个字节
3、静态代理和动态代理的区别,什么场景使用
区别:
静态代理通常只代理一个类,动态代理是代理一个接口下的多个实现类
使用场景
静态代理事先要知道代理的是什么 ,而动态代理不知道要代理什么东西,只有在运行时才知道;
动态代理是实现JDK里面的InvocationHandler接口里面的invoke方法,但注意的是代理的是接口,也就是说你的业务类必须要实现接口,通过Proxy里的newProxyInstance得到代理对象
还有一种动态代理CGLIB,代理的是类,不需要业务继承接口,通过派生的子类来实现代理。通过在运行时,动态修改字节码达到修改类的目的
4、Java的异常体系
Java把异常作为一种类,当做对象来处理。所有异常类的基类是Throwable类,两大子类分别是Error和Exception。
系统错误由Java虚拟机抛出,用Error类表示。Error类描述的是内部系统错误,例如Java虚拟机崩溃。这种情况仅凭程序自身是无法处理的,在程序中也不会对Error异常进行捕捉和抛出。
异常(Exception)又分为RuntimeException(运行时异常)和CheckedException(检查时异常),两者区别如下:
- RuntimeException:程序运行过程中才可能发生的异常。一般为代码的逻辑错误。例如:类型错误转换,数组下标访问越界,空指针异常、找不到指定类等等。
- CheckedException:编译期间可以检查到的异常,必须显式的进行处理(捕获或者抛出到上一层)。例如:IOException, FileNotFoundException等等
异常的处理
(1)throws (声明异常)
若方法中存在检查时异常,如果不对其捕获,那必须在方法头中显式声明该异常,以便于告知方法调用者此方法有异常,需要进行处理。
在方法中声明一个异常,方法头中使用关键字throws,后面接上要声明的异常。若声明多个异常,则使用逗号分割。
若是父类的方法没有声明异常,则子类继承方法后,也不能声明异常。
(2)try-catch(捕获异常)
若执行try块的过程中没有发生异常,则跳过catch子句。若是出现异常,try块中剩余语句不再执行。开始逐步检查catch块,判断catch块的异常类实例是否是捕获的异常类型。匹配后执行相应的catch块中的代码。如果异常没有在当前的方法中被捕获,就会被传递给该方法的调用者。这个过程一直重复,直到异常被捕获或被传给main方法(交给JVM来捕获)。
对于try..catch捕获异常的形式来说,对于异常的捕获,可以有多个catch。对于try里面发生的异常,他会根据发生的异常和catch里面的进行匹配(按照catch块从上往下匹配),如果有匹配的catch,它就会忽略掉这个catch后面所有的catch。
如果有finally的话进入到finally里面继续执行。
try ctach fianally 中有return 时,会先执行return ,但是不会返回。在执行完 finally 后 进行返回。
return 的是基本类型数据时, fianlly 里面的语句不会影响 return 的值,
return 的是引用类型数据时,此时已经确定了要返回对象的地址(地址一),后面 fianlly 里面的可以通过修改前面地址一中的内容修改返回的内容,但是如果将对象指向另一个地址(地址二),则不会影响返回的内容。因为返回的对象地址已经确定为地址一,只能通过修改地址一对象的内容修改返回的信息。
5、谈谈你对解析与分派的认识
解析:
方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期间是不可变的,即“编译时可知,运行不可以变”,这类目标的方法的调用称之为解析
解析调用一定是个静态的过程,在编译期就完全确定,在类加载的解析阶段就将涉及的符号引用全部转变为可以确定的直接引用,不会延迟到运行期再去完成。
分派:
分派是多态性的体现,Java虚拟机底层提供了我们开发中“重载”(Overload)“和重写”(Override)的底层实现。其中重载属于静态分派,而重写则是动态分派的过程
6、修改对象A的equals方法的签名,那么使用HashMap存放这个对象实例的时候,会调用哪个equals方法
会调用对象的equals方法,如果对象的equals方法没有被重写,equals方法和==都是比较栈内局部变量表中指向堆内存地址值是否相等
7、Java中实现多态的机制是什么
多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编译时不确定,在运行期间才确定,一个引用变量到底会指向哪个类的实例。这样就可以不用修改源程序,就可以让引用变量绑定到各种不同的类实现上。
Java实现多态有三个必要条件:继承、方法重写、向上转型,在多态中需要将子类的引用赋值给父类对象,只有这样该引用才能够具备调用父类方法和子类的方法。
8、说说你对Java反射的理解
在运行状态中,对任意一个类,都能知道这个类的所有属性和方法,对任意一个对象,都能调用它的任意一个方法和属性。这种能动态获取信息及动态调用对象方法的功能称为java语言的反射机制
9、说说你对Java注解的理解
Java注解(Annotation)也叫作元数据,以‘@注解名’在代码中存在,它是一种在源代码中标注的特殊标记,可以标注源代码中的类、属性、方法、参数等代码,主要用于创建文档,跟踪代码中的依赖性,甚至执行基本编译时检查
10、说一下泛型原理,并举例说明
Java泛型的实现方法:类型擦除
Java的泛型是伪泛型。在编译期间,所有的泛型信息都会被擦除掉
类型擦除:
Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中不包含泛型中的类型信息。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉,这个过程称为类型擦除。
如在代码中定义的List<object>和List<String>等类型,在编译后都会编程List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方法与C++模版机制实现方式之间的重要区别
11、Java中String的了解
(1)String类是final型,故String类不能被继承,它的成员方法也都默认为final方法。String对象一旦创建就固定不变了,对String对象的任何改变都不影响到原对象,相关的任何改变操作都会生成新的String对象
(2)String类是通过char数组来保存字符串的,String对equals方法进行了重写,比较的是值相等
注:Java为String类型提供了缓冲池机制,当使用双引号定义对象时,Java环境首先去字符串缓冲池寻找内容相同的字符串,如果存在就拿出来使用,否则就创建一个新的字符串放在缓冲池中。当使用new创建对象时,如果常量池中原来没有该字符串,就创建两个对象,如果字符串常量池中该字符串,就产生一个对象
12、String为什么要设计成不可变的
(1)字符串常量池需要String不可变
当创建一个String对象时,若此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。如果字符串变量允许改变,会导致各种逻辑错误,如改变一个对象会影响到另一个独立对象
(2)String对象可以缓存hashCode
字符串的不可变性保证了hash码的唯一性,因此可以缓存String的hashCode,这样不用每次去重新计算哈希码。在进行字符串比较时,可以直接比较hashCode,提高了比较性能
(3)安全性
String被许多java类用来当作参数,如url地址,文件path路径,反射机制所需的Strign参数等,若String可变,将会引起各种安全隐患
13、为什么重写equals方法一般必须重写hashcode方法
如果某个类重写了equals方法,但没有重写hashcode方法的话,equals判断两个值相等,但是hashcode的值不相等,如String类,这样就会造成歧义,如在存储散列集合时(如Set类),将会存储了两个值一样的对象