Java高频问题面试:
序号 | 文章 |
---|---|
1 | Java面试手册——高频问题总结(一) |
2 | Java面试手册——高频问题总结(二) |
3 | Java基础面试突击 |
4 | Java虚拟机——JVM总结 |
5 | JVM面试突击 |
Java 本身是一种面向对象的语言,最显著的特性有两个方面:
Java 通过字节码和 Java 虚拟机(JVM)这种跨平台的抽象,屏蔽了操作系统和硬件的细节,这也是实现「一次编译,到处执行」的基础。
Java 通过垃圾收集器(Garbage Collector)回收分配内存,大部分情况下,程序员不需要自己操心内存的分配和回收。
详细可参考:谈谈对Java平台的理解?Java是解释执行的这句话对吗?
JVM :Java Virtual Machine
是 Java 虚拟机,Java 程序需要运行在虚拟机上,不同的平台有自己的虚拟机,因此 Java 语言可以实现跨平台。
JRE :Java Runtime Environment
包括 Java 虚拟机和 Java 程序所需的核心类库等。核心类库主要是 java.lang
包:包含了运行 Java 程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、线程、异常处理类等,系统缺省加载这个包。如果想要运行一个开发好的 Java 程序,计算机中只需要安装 JRE 即可。
JDK :Java Development Kit
是提供给 Java 开发人员使用的,其中包含了 Java 的开发工具,也包括了 JRE。所以安装了 JDK,就无需再单独安装 JRE 了。其中的开发工具:编译工具(javac.exe),打包工具(jar.exe) 等。
这个说法不太准确。
我们开发的 Java 的源代码,首先通过 Javac 编译成为字节码(bytecode),然后,在运行时,通过 Java 虚拟机(JVM)内嵌的解释器将字节码转换成为最终的机器码。
但是常见的 JVM,比如我们大多数情况使用的 Oracle JDK 提供的 Hotspot JVM,都提供了 JIT(Just-In-Time)
编译器,也就是通常所说的动态编译器,JIT 能够在运行时将热点代码编译成机器码,这种情况下部分热点代码就属于编译执行,而不是解释执行了。
详细可参考:谈谈对Java平台的理解?Java是解释执行的这句话对吗?
字节码:Java 源代码经过虚拟机编译器编译后产生的文件(即扩展为.class 的文件),它不面向任何特定的处理器,只面向虚拟机。
采用字节码的好处:
通常把 Java 分为编译期和运行时。这里说的 Java 的编译和 C/C++是有着不同的意义的,Javac 的编译,编译 Java 源码生成“.class”文件里面实际是字节码,而不是可以直接执行的机器码。Java 通过字节码和 Java 虚拟机(JVM)这种跨平台的抽象,屏蔽了操作系统和硬件的细节,这也是实现“一次编译,到处执行”的基础。
Java有两大数据类型:基本数据类型和引用数据类型。
基本数据类型有8种:byte、short、int、long、float、double、char、boolean
对应的封装类:Byte、Short、Integer、Long、Float、Double、Character、Boolean
引用数据类型:类(class)、接口(interface)、数组([])
为什么需要封装类?
因为泛型类包括预定义的集合,使用的参数都是对象类型,无法直接使用基本数据类型,所以Java又提供了这些基本类型的封装类。
基本类型和封装类的区别?
& 运算符有两种用法:
&&: 短路与
a== b && b== c (当 a==b 为 false 则不会继续判断b是否等于c)
&& 运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是 true 整个表达式的值才是 true。
&& 之所以称为短路运算,是因为如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。
注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。
Java 是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是 Integer,从 Java
5 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。
int
是我们常说的整形数字,是 Java 的 8 个原始数据类型(Primitive Types,boolean、byte 、short、char、int、float、double、long)之一。Java 语言虽然号称一切都是对象,但原始数据类型是例外。
Integer
是 int 对应的包装类,它有一个 int 类型的字段存储数据,并且提供了基本操作,比如数学运算、int 和字符串之间转换等。在 Java 5 中,引入了自动装箱和自动拆箱功能(boxing/unboxing),Java 可以根据上下文,自动进行转换,极大地简化了相关编程。
详细请参考:int和Integer有什么区别?其中详细介绍了Integer 的值缓存问题。
变量:在程序执行的过程中,在某个范围内其值可以发生改变的量。从本质上讲,变量其实是内存中的一小块区域。
成员变量:方法外部,类内部定义的变量。
局部变量:类的方法中的变量。
区别如下:
作用域:成员变量:针对整个类有效。局部变量:只在某个范围内有效。(一般指的就是方法,语句体内)。
存储位置:成员变量:随着对象的创建而存在,随着对象的消失而消失,存储在堆内存中。局部变量:在方法被调用,或者语句被执行的时候存在,存储在栈内存中。当方法调用完,或者语句结束后,就自动释放。
生命周期:成员变量:随着对象的创建而存在,随着对象的消失而消失 局部变量:当方法调用完,或者语句结束后,就自动释放。
初始值:成员变量:有默认初始值。局部变量:没有默认初始值,使用前必须赋值。
== 比较两个对象在内存里是不是同一个对象,就是说在内存里的存储位置是一致的。两个String对象存储的值是一样的,但有可能在内存里存储在不同地方。
==比较的是引用而equals比较的是内容。public boolean equals(Object obj)
这个方法是由Object对象提供的,可以由子类重写。默认的实现只要当对象和自身进行比较时才会返回true,这时和==是等价的。
String s1 = "abc"; // 常量池
String s2 = new String("abc"); // 堆内存中
System.out.println(s1==s2); // false两个对象的地址值不一样。
System.out.println(s1.equals(s2)); // true
String s1="a"+"b"+"c";
String s2="abc";
System.out.println(s1==s2); // true
System.out.println(s1.equals(s2)); // true
String s1="ab";
String s2="abc";
String s3=s1+"c";
System.out.println(s3==s2); // false
System.out.println(s3.equals(s2)); // true
值传递是对基本型变量而言的,传递的是变量的一个副本,改变副本不影响原变量。
引用传递是对引用型变量而言的,传递是对象地址的一个副本,并不是原对象本身,索引对引用变量进行操作会同时改变原变量。
使用实参形参来解释:
值传递:方法调用时,实参将它的值传递给形参,此时内存中存在两个相等的基本类型,后面操作都是对形参修改,不影响实参。
引用传递:方法调用时,实参的引用(地址,而不是参数的值)被传递给形参,方法执行时,形参和实参的内容相同,指向同一块内存地址,方法执行中对引用的操作会影响实际对象。
注:String类型不可修改,相当于值传递。
基于反射实现。
反射机制是 Java 语言提供的一种基础功能,赋予程序在运行时自省(introspect,官方用语)的能力。通过反射我们可以直接操作类或者对象,比如获取某个对象的类定义,获取类声明的属性和方法,调用方法或者构造对象,甚至可以运行时修改类定义。
使用场景是什么?
AOP 通过(动态)代理机制可以让开发者从这些繁琐事项中抽身出来,大幅度提高了代码的抽象程度和复用度。
包装 RPC 调用:通过代理可以让调用者与实现者之间解耦。比如进行 RPC 调用,框架内部的寻址、序列化、反序列化等,对于调用者往往是没有太大意义的,通过代理,可以提供更加友善的界面。
(1)String为什么不可变?StringBuffer和StringBuilder为什么可变?
不可变对象是指一个对象的状态在对象被创建之后就不再发生变化,不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其它的对象,引用类型指向的对象的状态也不能改变。
String不可变是因为String类被声明一个final类,且类内部的value字节数组也是final的。private final char value[]
,所以string 对象是不可变的。
StringBuilder与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder中也是使用字符数组保存字符串,
char[] value ,这两种象都是可变的。
(2)线程安全?
String中的对象是不可变的,也就可以理解为常量,线程安全。
AbstractStringBuilder是 StringBuilder与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。 StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
(3)性能
每次对 String类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String对象。
StringBuffer 每次都会对 StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
详细请参考:String、StringBuffer、StringBuilder有什么区别?
HashMap中,如果要比较key是否相等,要同时使用这两个函数。
因为自定义的类的hashcode()方法继承于Object类,其hashcode码为默认的内存地址,这样即便有相同含义的两个对象,比较也是不相等的。HashMap的比较key是这样的,先求出key的hashcode(),比较其值是否相等,若相等再比较equals(),若相等则认为他们是相等的。若equals()不相等则认为不相等。如果只重写hashcode()不重写equals()方法,当比较equals()时只是看他们是否为同一对象(即进行内存地址的比较),所以必定要两个方法一起重写。
总结:
重写 equals 时必须重写 hashCode 方法?
因为Set存储的是不重复对象,依据hashCode和equals进行判断,所以Set存储必须重写这两个方法;
如果自定义对象作为Map的键,必须重写hashCode和equals;
String重写了hashCode和equals方法,可以使用String对象作为key使用。
重写equals可以让我们判断两个对象是否相同
Object中定义的hashcode方法生成的哈希码能保证同一个类的对象的哈希码一定是不同的
当equals 返回为true,我们在逻辑上可以认为是同一个对象,但是查看哈希码,发现哈希码不同,和equals方法的返回值结果违背
Object中定义的hashcode方法生成的哈希码跟对象的本身属性值是无关的
重写hashCode之后我们可以自定义hash值的生成规则,可以通过对象的属性值计算出hash码
HashMap中,借助equals和hashcode方法来完成数据的存储
将根据对象的内容查询转换为索引的查询
(1)相同点:
Exception
和 Error
都是继承了 Throwable
类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。
Exception 和 Error 体现了 Java 平台设计者对不同异常情况的分类。
(2)异常使用规范:
(3)区别:
Exception
:是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。
Exception 又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。
不检查异常就是所谓的运行时异常,类似 NullPointerException、ArrayIndexOutOfBoundsException 之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。
Error
:此类错误一般表示代码运行时 JVM 出现问题。通常有 Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。比如 OutOfMemoryError:内存不足错误;StackOverflowError:栈溢出错误。此
类错误发生时,JVM 将终止线程。
绝大多数导致程序不可恢复,这些错误是不受检异常,非代码性错误。因此,当此类错误发生时,应用程序不应该去处理此类错误。按照 Java 惯例,我们是不应该实现任何新的 Error 子类的!
(4)JVM 如何处理异常?
在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。
创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。
JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。
当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。
(5)NoClassDefFoundError 和 ClassNotFoundException 区别?
NoClassDefFoundError
是一个 Error 类型的异常,是由 JVM 引起的,不应该尝试捕获这个异常。
引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是变异后被删除了等原因导致;
ClassNotFoundException
是一个受查异常,需要显式地使用 try-catch 对其进行捕获和处理,或在方法签名中用 throws 关键字进行声明。
当使用 Class.forName , ClassLoader.loadClass或 ClassLoader.findSystemClass
动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会抛出该异常;
另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。
详细请参考Exception和Error有什么区别?
(6)Java 常见异常有哪些?
java.lang.IllegalAccessError
:违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。
java.lang.InstantiationError
:实例化错误。当一个应用试图通过 Java 的new 操作符构造一个抽象类或者接口时抛出该异常。
java.lang.OutOfMemoryError
:内存不足错误。当可用内存不足以让 Java 虚拟机分配给一个对象时抛出该错误。
java.lang.StackOverflowError
:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。
java.lang.ClassCastException
:类造型异常。假设有类 A 和 B(A 不是 B 的父类或子类),O 是 A 的实例,那么当强制将 O 构造为类 B 的实例时抛出该异常。该异常经常被称为强制类型转换异常。
java.lang. ClassNotFoundException
:找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历 CLASSPAH 之后找不到对应名称的 class 文件时,抛出该异常。
java.lang.ArithmeticException
:算术条件异常。譬如:整数除零等。
java.lang. ArrayIndexOutOfBoundsException
:数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。
除了名字相似,他们毫无关系!!!
final
可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。finally
一般作用在 try-catch 代码块中,在处理异常的时候,通常我们将一定要执行的代码方法 finally 代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。finalize
是一个方法,属于 Object 类的一个方法,而 Object 类是所有类的父类,Java 中允许使用 finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。final 有什么用?
用于修饰类、属性和方法;
try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?
会执行,在 return 前执行。
注意:在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块,try中的 return 语句不会立马返回调用者,而是记录下返回值待 finally 代码块执行完毕之后再向调用者返回其值,然后如果在 finally 中修改了返回值,就会返回修改后的值。
显然,在 finally 中返回或者修改返回值会对程序造成很大的困扰,C#中直接用编译错误的方式来阻止程序员干这种龌龊的事情,Java 中也可以通过提升编译器的语法检查级别来产生警告或错误。
不同的引用类型,主要体现的是对象不同的可达性(reachable)状态和对垃圾收集的影响。
强引用:通过 new 创建的对象就是强引用,强引用指向一个对象,就表示这个对象还活着,垃圾回收不会去收集。
软引用:是一种相对强引用弱化一些的引用,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
弱引用:ThreadlocalMap 中的 key 就是用了弱引用,因为ThreadlocalMap被 thread对象持有,所以如果是强引用的话,只有当 thread 结束时才能被回收,而弱引用则可以在使用完后立即回收,不必等待 thread 结束。
虚引用:“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 ( ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
详细请参考:Java强引用、软引用、弱引用、虚引用有什么区别?
(1)什么是Java反射机制?
Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。反射被视为动态语言的关键。
(2)举例什么地方用到反射机制?
(3)Java反射机制的作用
(4)Java反射机制类
java.lang.Class; //类
java.lang.reflect.Constructor; //构造器
java.lang.reflect.Field; //类的成员变量
java.lang.reflect.Method; //类的方法
java.lang.reflect.Modifier; //访问权限
(5)反射机制优缺点
优点:运行期类型的判断,动态加载类,提高代码灵活度。
缺点:性能瓶颈:反射相当于一系列解释操作,通知JVM要做的事情,性能比直接的Java代码要慢的多。
在多线程程序中,诸如++i 或 i++等运算不具有原子性
,是不安全的线程操作之一。
通常我们会使用 synchronized
将该操作变成一个原子操作,但 JVM 为此类操作特意提供了一些同步类,使得使用更方便,且使程序运行效率变得更高。通过相关资料显示,通常AtomicInteger
的性能是 ReentantLock 的好几倍。
此处 AtomicInteger,一个提供原子操作的 Integer 的类,常见的还有AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference 等,他们的实现原理相同,区别在与运算对象类型的不同。令人兴奋地,还可以通过 AtomicReference将一个对象的所有操作转化成原子操作。
AtomicInteger >>> Unsafe >>> cas >>> aba
Stream作为Java8的一大亮点,它与java.io包里的InputStream和OutputStream是完全不同的概念。它是对容器对象功能的增强,它专注于对容器对象进行各种非常便利、高效的聚合操作或者大批量数据操作
。
Stream API借助于同样新出现的Lambda表达式,极大的提高编程效率和程序可读性。同时,它提供串行和并行两种模式进行汇聚操作
,并发模式能够充分利用多核处理器的优势,使用fork/join并行方式来拆分任务和加速处理过程
。所以说,Java8中首次出现的 java.util.stream是一个函数式语言+多核时代综合影响的产物。
Stream有如下三个操作步骤:
当数据源中的数据上了流水线后,这个过程对数据进行的所有操作都称为“中间操作”。中间操作仍然会返回一个流对象,因此多个中间操作可以串连起来形成一个流水线。比如map (mapToInt, flatMap 等)、filter、distinct、sorted、peek、limit、skip、parallel、sequential、unordered。
当所有的中间操作完成后,若要将数据从流水线上拿下来,则需要执行终止操作。终止操作将返回一个执行结果,这就是你想要的数据。比如:forEach、forEachOrdered、toArray、reduce、collect、min、max、count、anyMatch、allMatch、noneMatch、findFirst、findAny、iterator。
多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何处理!而在终止操作时一次性全部处理,称作“惰性求值”。
stream并行原理: 其实本质上就是在ForkJoin上进行了一层封装,将Stream 不断尝试分解成更小的split,然后使用fork/join 框架分而治之, parallize使用了默认的ForkJoinPool.common 默认的一个静态线程池
.
面向过程:
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展。
面向对象:
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护。
缺点:性能比面向过程低。
面向过程是具体化的,流程化的,解决一个问题,你需要一步一步的分析,一步一步的实现。
面向对象是模型化的,你只需抽象出一个类,这是一个封闭的盒子,在这里你拥有数据也拥有解决问题的方法。需要什么功能直接使用就可以了,不必去一步一步的实现,至于这个功能是如何实现的,管我们什么事?我们会用就可以了。
面向对象的底层其实还是面向过程,把面向过程抽象成类,然后封装,方便我们使用的就是面向对象了。
面向对象编程因为其具有丰富的特性(封装、抽象、继承、多态),可以实现很多复杂的设计思路,是很多设计原则、设计模式等编码实现的基础。
封装:
把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。
通过封装,只需要暴露必要的方法给调用者,调用者不必了解背后的业务细节,用错的概率就减少。
继承:
使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。
通过使用继承我们能够非常方便地复用以前的代码,需要注意的是,过度使用继承,层级深就会导致代码可读性和可维护性变差。
关于继承如下 3 点请记住:
多态:
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定。
即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
在 Java 中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
多态也是很多设计模式、设计原则、编程技巧的代码实现基础,比如策略模式、基于接口而非实现编程、依赖倒置原则、里式替换原则、利用多态去掉冗长的 if-else 语句等等。
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
多态分为编译时多态和运行时多态。
其中编译时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。
而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。
Java 实现多态有三个必要条件:继承、重写、向上转型。
继承:在多态中必须存在有继承关系的子类和父类。
重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
向上转型:在多态中需要将父类的引用指向子类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分。
重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为 private 则子类中就不是重写。
SOLID是面向对象编程的一种设计原则,对于每一种设计原则,我们需要掌握它的设计初衷,能解决哪些编程问题,有哪些应用场景。
抽象类是用来捕捉子类的通用特性的。接口是抽象方法的集合。
接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守这样一个原则:
相同点:
接口:
接口定义了协议,是面向对象编程(封装、继承多态)基础,通过接口我们能很好的实现单一职责、接口隔离、内聚。
public static final
的意义;Java8 中接口中引入默认方法和静态方法,并且不用强制子类来实现它。以此来减少抽象类和接口之间的差异。
抽象类:
抽象类是不能实例化的类,用 abstract 关键字修饰 class,其目的主要是代码重用。
从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
除了不能实例化,形式上和一般的 Java 类并没有太大区别。
可以有一个或者多个抽象方法,也可以没有抽象方法。抽象类大多用于抽取相关 Java类的共用方法实现或者是共同成员变量,然后通过继承的方式达到代码复用的目的。
抽象类能用 final 修饰么?
不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类。
抽象类与接口的区别:
public static final
类型的。注:JDK 1.8 以后,接口里可以有静态方法和方法体了。
注:JDK 1.8 以后,接口允许包含具体实现的方法,该方法称为"默认方法",默认方法使用 default 关键字修饰。
Java中的类不支持多继承,只支持单继承。但是Java中的接口支持多继承,即一个子接口可以继承多个父接口。(接口的作用是用来扩展对象的功能,一个子接口继承多个父接口,说明子接口扩展了多个功能,当类实现接口时,就扩展了相应的功能。)
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
假定我们有这样一个需求:写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,该如何实现?
答案是可以使用 Java 泛型。使用 Java 泛型的概念,我们可以写一个泛型方法来对一个对象数组排序。然后,调用该泛型方法来对整型数组、浮点数数组、字符串数组等进行排序。
(1)泛型中上下界的定义
上界: extends T>
表示该通配符所代表的类型是T类型的子类。
下界: super T>表示该通配符所代表的类型是T类型的父类。
(2)上界和下界的特点
上界的list只能get,不能add(确切的说不能add除null之外的对象,包括Object)
下界的list只能add,不能get
(3)上下界转型
上界 extends Fruit>
表示所有继承Fruit的子类,但是具体是哪个子类,无法确定,所以调用add的时候,要add什么类型,谁也不知道。但是get的时候,不管是什么子类,不管追溯多少辈,肯定有个父类是Fruit,所以,我都可以用最大的父类Fruit接着,也就是把所有的子类向上转型为Fruit。
下界<? super Apple>
, 表示Apple的所有父类,包括Fruit, 一直可以追溯到老祖宗Object 。那么当我add的时候,我不能add Apple的父类,因为不能确定List里面存放的到底是哪个父类。但是我可以add Apple 及其子类。因为不管我的子类是什么类型,它都可以向上转型为Apple及其所有的父类甚至转型为Object 。但是当我get的时候,Apple的父类这么多,我用什么接着呢,除了0bject,其他的都接不住。
Object是所有类的父类,任何类都默认继承Object
Clone()
创建并返回对象的一个副本。只有实现了Cloneable接口才可以调用该方法,否则会抛出CloneNotSupportedException
异常。
equals()
指示某个其他对象是否与此对象“相等”,子类一般需要重写该方法。
hashCode()
该方法用于哈希查找,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到,返回该对象的哈希码值。
getClass()
final方法,获得运行时类型。
wait()
使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。
调用该方法后当前线程进入睡眠状态,直到以下事件发生
notify()
唤醒在该对象上等待的某个线程。
notifyAll()
唤醒在该对象上等待的所有线程。
toString()
转换成字符串,一般子类都有重写,否则打印句柄。
finalize()
当垃圾回收器确定不存在该对象的更多引用时,由对象的垃圾回收器调用此方法。
this 是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。
this 的用法在 java 中大体可以分为 3 种:
super 可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。
super 也有三种用法:
Java提供了只包含一个compareTo()
方法的Comparable
接口。这个方法可以给两个对象排序。具体来说,它返回负数,0,正数来表明输入对象小于,等于,大于已经存在的对象。
Java提供了包含compare()
和 equals()
两个方法的Comparator
接口。compare()
方法用来给两个输入参数排序,返回负数,0,正数表明第一个参数是小于,等于,大于第二个参数。equals()
方法需要一个对象作为参数,它用来决定输入参数是否和comparator
相等。只有当输人参数也是一个comparator
并且输入参数和当前comparator
的排序结果是相同的时候,这个方法才返回true。
(1)抽象的 (abstract)方法是否可同时是静态的(static)?
抽象方法将来是要被重写的,而静态方法是不能重写的,所以这个是错误的。
(2)是否可以从一个静态(static)方法内部发出对非静态方法的调用?
不可以,静态方法只能访问静态成员,非静态方法的调用要先创建对象。
(3)static可否用来修饰局部变量?
static不允许用来修饰局部变量。
(4)内部类与静态内部类的区别?
静态内部类相对与外部类是独立存在的,在静态内部类中无法直接访问外部类中变量、方法。如果要访问的话,必须要new一个外部类的对象,使用new出来的对象来访问。但是可以直接访问静态的变量、调用静态的方法;
普通内部类作为外部类一个成员而存在,在普通内部类中可以直接访问外部类属性,调用外部类的方法。
如果外部类要访问内部类的属性或者调用内部类的方法,必须要创建一个内部类的对象,使用该对象访问属性或者调用方法。
如果其他的类要访问普通内部类的属性或者调用普通内部类的方法,必须要在外部类中创建一个普通内部类的对象作为一个属性,外同类可以通过该属性调用普通内部类的方法或者访问普通内部类的属性。
如果其他的类要访问静态内部类的属性或者调用静态内部类的方法,直接创建一个静态内部类对象即可。
(5)Java中是否可以覆盖(override)一个private或者是static的方法?
Java中static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的。static方法跟类的任何实例都不相关,所以概念上不适用。
注:为了方便扩展每部分内容,不至于过长,将总结分为两部分,Java集合和垃圾回收的问题请查看Java面试手册——高频问题总结(二)