1.Java 基础
1.1 语法基础
-
封装
利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。
-
继承
继承实现了 IS-A 关系,例如 Cat 和 Animal 就是一种 IS-A 关系,因此 Cat 可以继承自 Animal,从而获得 Animal 非 private 的属性和方法。
继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。
Cat 可以当做 Animal 来使用,也就是说可以使用 Animal 引用 Cat 对象。父类引用指向子类对象称为 向上转型
-
多态
多态分为编译时多态和运行时多态:
- 编译时多态主要指方法的重载
- 运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定
运行时多态有三个条件:
- 继承
- 覆盖(重写)
- 向上转型
1.1.1 tips
1.a = a + b 与 a += b 的区别
首先,我们从原理上做简单的分析,a+=b是物理上是只做一次运算,逻辑上是与a=a+b一致的,a=a+b实质上是做了两次运算的。但机器的编译和人类的思考方式是不同的,这是底层人员在设计时就做了规定的。我们今天主要讲述机器的思考逻辑,他们的不同就可以分为以下两种情况:
一、当a和b的数据类型相同时,a+=b的效率是高于a=a+b的。
二、当a和b的数据类型不同时,同样需要分为两种情形:
1.a的数据类型小于b(向上兼容),a+=b的精度会丢失,a=a+b会直接报错。
2.a的数据类型大于b(向下兼容),两者都会成功编译。
+= 隐式的将加操作的结果类型强制转换为持有结果的类型。如果两个整型相加,如 byte、short 或者 int,首先会将它们提升到 int 类型,然后在执行加法操作。
byte a=1;
byte b=2;
byte c;
c=a+b; //这样是计算不出c,是错误的
c=a+1; //这样也是不能计算c的
c=64+1; //为什么这样就能计算c,在Java中这是什么原理啊?
运算符对基本类型的影响
当使用+、-、*、/、%运算符对基本类型进行运算时,遵循如下规则:
只要两个操作数中有一个是double类型的,另一个将会被转换成double类型,并且结果也是double类型;
否则,只要两个操作数中有一个是float类型的,另一个将会被转换成float类型,并且结果也是float类型;
否则,只要两个操作数中有一个是long类型的,另一个将会被转换成long类型,并且结果也是long类型;
否则,两个操作数(包括byte、short、int、char)都将会被转换成int类型,并且结果也是int类型。
2.3*0.1 == 0.3 将会返回什么? true 还是 false?
false,因为有些浮点数不能完全精确的表示出来。
3.能在 Switch 中使用 String 吗?
从 Java 7 开始,我们可以在 switch case 中使用字符串,但这仅仅是一个语法糖。内部实现在 switch 中使用字符串的 hash code。
本来只可以是八大基础数据类型。
4.对equals()和hashCode()的理解?
-
为什么在重写 equals 方法的时候需要重写 hashCode 方法?
因为有强制的规范指定需要同时重写 hashcode 与 equals 是方法,许多容器类,如 HashMap、HashSet 都依赖于 hashcode 与 equals 的规定。
-
有没有可能两个不相等的对象有相同的 hashcode?
有可能,两个不相等的对象可能会有相同的 hashcode 值,这就是为什么在 hashmap 中会有冲突。相等 hashcode 值的规定只是说如果两个对象相等,必须有相同的hashcode 值,但是没有关于不相等对象的任何规定。
-
两个相同的对象会有不同的 hash code 吗?
不能,根据 hashcode 的规定,这是不可能的。
equals方法
Object类中的方法,默认检测一个对象是否等于另外一个对象,即判断两个对象是否具有相同的引用。
在实际使用过程中,比较两个对象的引用往往没有任何意义,需要比较的是对象的状态,即对象的属性是否相等。所以在需要比较类之间是否相等时,都需要重写equals方法。
如何处理子类与父类之间equals()?
现在有一个子类Manager继承Employee类,并重写父类的equals方法:
但是下面的测试很有意思:
manager.equals(employee1)抛出类不能转换的异常。我们知道java语言规范要求equals方法具有自反性、对称性,传递性和一致性;这里明显违反了对称性,即对于任何引用x和y,当且仅当y.equals(x)返回true,x.equals(y)也应该返回true。
那么是哪里出了问题呢?
子类Manager重写equlas(),会把object参数转换成Manager类型的对象,这里就是父类employee1转换成子类manager,明显会报异常,也就是说equals方法没有校验到object参数和this不属于同一个类的情况。即instanceof关键词并不能解决子类的问题。可以把
hashCode()方法
hashCode()方法也是Object类中的一个默认方法,它默认作用是返回对象的存储地址,方法返回一个整型值的散列码;如果某个对象重写了equals()方法,如上面Employee类通过name属性比较两个employee对象是否相等,由于Employee类没有重写hashCode()方法,所以即使employee1.equals(employe2)返回true,employee1和employee2的hashCode值也不一样,即在内存中可以同时存在,那么比较的意义也就不存在了。因此,重写equals()方法同时也需要重写hashCode()方法,即如果x.equals(y)返回true,那么x.hashCode()值必须与y.hashCode()值相同。
str对象和stri对象的hashCode值相等,这是因为String类重写了hashCode方法;而sBuffer和sBuffer2的hashCode值不相等,这是因为StringBuffer类继承Object类默认的hashCode(),返回的是对象的存储地址。
所以,如果要比较两个对象是否相等,除了重写equals()方法还需要重写hashCode()方法。注意equal()比较的属性值与计算hashCode的属性值要一致;如Employee类根据name属性判断对象是否相等,则hashCode()方法也需要根据name属性值计算hashCode值。
5.final、finalize 和 finally 的不同之处?
- final 是一个修饰符,可以修饰变量、方法和类。如果 final 修饰变量,意味着该变量的值在初始化后不能被改变。
- Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的,但是什么时候调用 finalize 没有保证。
- finally 是一个关键字,与 try 和 catch 一起用于异常的处理。finally 块一定会被执行,无论在 try 块中是否有发生异常。
6.String、StringBuffer与StringBuilder的区别?
第一点: 可变和适用范围。String对象是不可变的,而StringBuffer和StringBuilder是可变字符序列。每次对String的操作相当于生成一个新的String对象,而对StringBuffer和StringBuilder的操作是对对象本身的操作,而不会生成新的对象,所以对于频繁改变内容的字符串避免使用String,因为频繁的生成对象将会对系统性能产生影响。
第二点: 线程安全。String由于有final修饰,是immutable的,安全性是简单而纯粹的。StringBuilder和StringBuffer的区别在于StringBuilder不保证同步,也就是说如果需要线程安全需要使用StringBuffer,不需要同步的StringBuilder效率更高
7.接口与抽象类的区别?
- 一个子类只能继承一个抽象类, 但能实现多个接口
- 抽象类可以有构造方法, 接口没有构造方法
- 抽象类可以有普通成员变量, 接口没有普通成员变量
- 抽象类和接口都可有静态成员变量, 抽象类中静态成员变量访问类型任意,接口只能public static final(默认)
- 抽象类可以没有抽象方法, 抽象类可以有普通方法;接口在JDK8之前都是抽象方法,在JDK8可以有default方法,在JDK9中允许有私有普通方法
- 抽象类可以有静态方法;接口在JDK8之前不能有静态方法,在JDK8中可以有静态方法,且只能被接口类直接调用(不能被实现类的对象调用)
- 抽象类中的方法可以是public、protected; 接口方法在JDK8之前只有public abstract,在JDK8可以有default方法,在JDK9中允许有private方法
8.this() & super()在构造方法中的区别?
- 调用super()必须写在子类构造方法的第一行, 否则编译不通过
- super从子类调用父类构造, this在同一类中调用其他构造均需要放在第一行
- 尽管可以用this调用一个构造器, 却不能调用2个
- this和super不能出现在同一个构造器中, 否则编译不通过
- this()、super()都指的对象,不可以在static环境中使用
- 本质this指向本对象的指针。super是一个关键字
Java中this与super的区别_java 的super赋值和this赋值有区别吗-CSDN博客
所以综上所述,super()与this()的区别主要有以下:
不同点:
1、super()主要是对父类构造函数的调用,this()是对重载构造函数的调用
2、super()主要是在继承了父类的子类的构造函数中使用,是在不同类中的使用;this()主要是在同一类的不同构造函数中的使用
相同点:
1、super()和this()都必须在构造函数的第一行进行调用,否则就是错误的java 中 this 和 super 说明及在构造器中super()和this()相互调用执行顺序_java中什么是super、this,在构造方法调用顺序是什么?-CSDN博客
9.Java移位运算符?
java中有三种移位运算符
<<
:左移运算符,x << 1
,相当于x乘以2(不溢出的情况下),低位补0>>
:带符号右移,x >> 1
,相当于x除以2,正数高位补0,负数高位补1>>>
:无符号右移,忽略符号位,空位都以0补齐
1.2 泛型
为什么需要泛型?
适用于多种数据类型执行相同的代码
如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个add方法;通过泛型,我们可以复用为一个方法
泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型)
泛型类如何定义使用
泛型接口如何定义使用?
泛型方法如何定义使用?
泛型方法,是在调用方法的时候指明泛型的具体类型。
- 定义泛型方法语法格式
- 调用泛型方法语法格式
说明一下,定义泛型方法时,必须在返回值前边加一个
<T>
,来声明这是一个泛型方法,持有一个泛型T
,然后才可以用泛型T作为方法的返回值。
Class<T>
的作用就是指明泛型的具体类型,而Class<T>
类型的变量c,可以用来创建泛型类的对象。为什么要用变量c来创建对象呢?既然是泛型方法,就代表着我们不知道具体的类型是什么,也不知道构造方法如何,因此没有办法去new一个对象,但可以利用变量c的newInstance方法去创建对象,也就是利用反射创建对象。
泛型方法要求的参数是
Class<T>
类型,而Class.forName()
方法的返回值也是Class<T>
,因此可以用Class.forName()
作为参数。其中,forName()
方法中的参数是何种类型,返回的Class<T>
就是何种类型。在本例中,forName()
方法中传入的是User类的完整路径,因此返回的是Class<User>
类型的对象,因此调用泛型方法时,变量c的类型就是Class<User>
,因此泛型方法中的泛型T就被指明为User,因此变量obj的类型为User。当然,泛型方法不是仅仅可以有一个参数
Class<T>
,可以根据需要添加其他参数。为什么要使用泛型方法呢?
因为泛型类要在实例化的时候就指明类型,如果想换一种类型,不得不重新new一次,可能不够灵活;而泛型方法可以在调用的时候指明类型,更加灵活。
泛型的上限和下限?
如何理解Java中的泛型是伪泛型?
泛型中类型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。
1.3 注解
注解的作用?
注解是JDK1.5版本开始引入的一个特性,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。它主要的作用有以下四方面:
- 生成文档,通过代码里标识的元数据生成javadoc文档。
- 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证。
- 编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码。
- 运行时动态处理,运行时通过代码里标识的元数据动态处理,例如使用反射注入实例。
注解的常见分类?
- Java自带的标准注解,包括
@Override
、@Deprecated
和@SuppressWarnings
,分别用于标明重写某个方法、标明某个类或方法过时、标明要忽略的警告,用这些注解标明后编译器就会进行检查。 - 元注解,元注解是用于定义注解的注解,包括
@Retention
、@Target
、@Inherited
、@Documented
@Retention
用于标明注解被保留的阶段@Target
用于标明注解使用的范围@Inherited
用于标明注解可继承@Documented
用于标明是否生成javadoc文档
- 自定义注解,可以根据自己的需求定义注解,并可用元注解对自定义注解进行注解。
1.4 异常
Throwable 是 Java 语言中所有错误与异常的超类。
- Error 类及其子类:程序中无法处理的错误,表示运行应用程序中出现了严重的错误。
- Exception 程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。
- 运行时异常
都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。
- 非运行时异常 (编译异常)
是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常
可查的异常(checked exceptions)和不可查的异常(unchecked exceptions)区别?
- 可查异常(编译器要求必须处置的异常):
正确的程序在运行中,很容易出现的、情理可容的异常状况。可查异常虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,就必须采取某种方式进行处理。
除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。
- 不可查异常(编译器不要求强制处置的异常)
包括运行时异常(RuntimeException与其子类)和错误(Error)。
1.4.1 tips
throw和throws的区别?
- 异常的申明(throws)
在Java中,当前执行的语句必属于某个方法,Java解释器调用main方法执行开始执行程序。若方法中存在检查异常,如果不对其捕获,那必须在方法头中显式声明该异常,以便于告知方法调用者此方法有异常,需要进行处理。 在方法中声明一个异常,方法头中使用关键字throws,后面接上要声明的异常。若声明多个异常,则使用逗号分割。如下所示:
- 异常的抛出(throw)
如果代码可能会引发某种错误,可以创建一个合适的异常类实例并抛出它,这就是抛出异常。如下所示:
throw代表动作,表示抛出一个异常的动作;
throws代表一种状态,代表方法可能有异常抛出throw用在方法实现中,而throws用在方法声明中;
throw只能用于抛出一种异常,而throws可以抛出多个异常
throw和throws的区别是什么?_java 异常throw 和 throws 区别?-CSDN博客
Java中throw和throws介绍_java throw语句怎么写-CSDN博客
所以,就像我一开始说的:你用throws不一定会抛出异常,它只是代表这个方法可能抛出这个异常,像打个预防针一样;你要用throw那一定会抛出异常,因为throw是一个行为,代表抛出异常的动作。
Java中的throw和throws:异常处理详解_java throw-CSDN博客
Java 7 的 try-with-resource?
异常的底层?
1.5 反射
什么是反射?
反射的使用?
getName、getCanonicalName与getSimpleName的区别?
- getSimpleName:只获取类名
- getName:类的全限定名,jvm中Class的表示,可以用于动态加载Class对象,例如Class.forName。
- getCanonicalName:返回更容易理解的表示,主要用于输出(toString)或log打印,大多数情况下和getName一样,但是在内部类、数组等类型的表示形式就不同了。