java面试—Day4
我们的目标是星辰大海,而非人间烟尘
文章目录
1、成员内部类、静态内部类、方法内部类(局部内部类)和匿名内部类的理解,以及项目中的应用
成员内部类:
最普通的内部类,它的定义位于一个类的内部,这样看起来,成员内部类相当于外部类的一个成员,成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员)。不过需要注意的是,当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,形式如下:
外部类.this.成员变量
外部类.this.成员方法
虽然成员内部类可以无条件的访问外部类的成员,而外部类想访问成员内部类的成员却不是那么随心所欲了。成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提必须存在一个外部类对象。创建成员内部类对象的一般方式如下:
public class Test {
public static void main(String[] args) {
//第一种方式:
Outter outter = new Outter();
Outter.Inner inner = outter.new Inner(); //必须通过Outter对象来创建
//第二种方式:
Outter.Inner inner1 = outter.getInnerInstance();
}
}
class Outter {
private Inner inner = null;
public Outter() {
}
public Inner getInnerInstance() {
if(inner == null)
inner = new Inner();
return inner;
}
class Inner {
public Inner() {
}
}
}
内部类可以拥有private、protected、public以及包访问权限。如果成员内部类用private修饰,则只能在外部类的内部访问,如果用public修饰,则任何地方都能访问,如果用protected修饰,则只能在同一个包下或者继承外部类的情况下访问,如果是默认访问权限,则只能是同一个包下访问。这一点和外部类有一点不一样,外部类只能被public和包访问两种权限修饰。由于成员内部类看起来像外部类的一个成员,所以可以像类的成员一样拥有多种修饰权限。
静态内部类:
静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖外部类的,这点和类的静态成员属性有点相似,并且它不能使用外部类的非static成员变量或者方法。这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依赖于具体的对象。
局部内部类:
局部内部类是定义在一个方法或者一个作用域里面的的类,它和成员内部类的区别在于局部内部类的访问权限仅限于方法内或者该作用域内。注意,局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。
匿名内部类:
匿名内部类不能有访问修饰符和static修饰符的,一般用于编写监听代码。匿名内部类是唯一一个没有构造器的类。正因为没有构造器,所以匿名内部类的适用范围非常有限,大部分匿名内部类用于接口回调。一般来说,匿名内部类用于继承其他类或者实现接口,并不需要增加额外的方法,只是对继承方法的实现或者是重写。
应用场景:
-
最重要的一点,每个内部类都能独立的继承一个接口的实现,无论外部类是否已经继承了某个接口的实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整。
- 方便将存在一定逻辑关系的类组织在一起,又可以对外界隐蔽。
- 方便编写事件驱动程序
- 方便编写线程代码
- 方便将存在一定逻辑关系的类组织在一起,又可以对外界隐蔽。
总结:
对于成员内部类,必须先产生外部类的实例化对象,才能产生内部类的实例化对象。而非静态内部类不用产生外部类的实例化对象即可产生内部类的实例化对象。
创建静态内部类对象的一般形式:
外部类类名.内部类类名 xxx = new 外部类类名.内部类类名()
创建成员内部类对象的一般形式:
外部类类名.内部类类名 xxx = new 外部类对象名.new 内部类类名()
参考自:[http://www.cnblogs.com/latter/p/5665015.html]
2、String转换成Integer的方式以及原理
String转int:
int i = Integer.parseInt(s);
int i = Integer.valueOf(s).intValue();
int转String:
String s = i + "";
String s = Integer.toString(i);
String s = String.valueOf(i);
源码:
public static int parseInt(String s, int radix) throws NumberFormatException
{
/*
* WARNING: This method may be invoked early during VM initialization
* before IntegerCache is initialized. Care must be taken to not use
* the valueOf method.
*/
// 下面三个判断好理解,其中表示进制的 radix 要在(2~36)范围内
if (s == null) {
throw new NumberFormatException("null");
}
if (radix < Character.MIN_RADIX) {
throw new NumberFormatException("radix " + radix +
" less than Character.MIN_RADIX");
}
if (radix > Character.MAX_RADIX) {
throw new NumberFormatException("radix " + radix +
" greater than Character.MAX_RADIX");
}
int result = 0; // 表示结果, 在下面的计算中会一直是个负数,
// 假如说 我们的字符串是一个正数 "7" ,
// 那么在返回这个值之前result保存的是 -7,
// 这个可能是为了保持正数和负数在下面计算的一致性
boolean negative = false;
int i = 0, len = s.length();
//limit 默认初始化为 最大正整数的 负数 ,假如字符串表示的是正数,
//那么result(在返回之前一直是负数形式)就必须和这个最大正数的负数来比较,判断是否溢出
int limit = -Integer.MAX_VALUE;
int multmin;
int digit;
if (len > 0) { // 首先是对第一个位置判断,是否含有正负号
char firstChar = s.charAt(0);
if (firstChar < '0') { // Possible leading "+" or "-"
if (firstChar == '-') {
negative = true;
// 这里,在负号的情况下,判断溢出的值就变成了 整数的 最小负数了。
limit = Integer.MIN_VALUE;
} else if (firstChar != '+')
throw NumberFormatException.forInputString(s);
if (len == 1) // Cannot have lone "+" or "-"
throw NumberFormatException.forInputString(s);
i++;
}
multmin = limit / radix;
// 这个是用来判断当前的 result 在接受下一个字符串位置的数字后会不会溢出。
// 原理很简单,为了方便,拿正数来说
// (multmin result 在计算中都是负数),假如是10
// 进制,假设最大的10进制数是 21,那么multmin = 21/10 = 2,
// 如果我此时的 result 是 3 ,下一个字符c来了,result即将变成
// result = result * 10 + c;那么这个值是肯定大于 21 ,即溢出了,
// 这个溢出的值在 int里面是保存不了的,不可能先计算出
// result(此时的result已经不是溢出的那个值了) 后再去与最大值比较。
// 所以要通过先比较 result < multmin (说明result * radix 后还比 limit 小)
while (i < len) {
// Accumulating negatively avoids surprises near MAX_VALUE
digit = Character.digit(s.charAt(i++),radix);
if (digit < 0) {
throw NumberFormatException.forInputString(s);
}
// 这里就是上说的判断溢出,由于result统一用负值来计算,所以用了 小于 号
// 从正数的角度看就是 reslut > mulmin 下一步reslut * radix 肯定是 溢出了
if (result < multmin) {
throw NumberFormatException.forInputString(s);
}
result *= radix;
// 这里也是判断溢出, 由于是负值来判断,相当于 (-result + digit)> - limit
// 但是不能用这种形式,如果这样去比较,那么得出的值是肯定判断不出溢出的。
// 所以用 result < limit + digit 很巧妙
if (result < limit + digit) {
throw NumberFormatException.forInputString(s);
}
result -= digit; // 加上这个 digit 的值 (这里减就相当于加)
}
} else {
throw NumberFormatException.forInputString(s);
}
return negative ? result : -result; // 正数就返回 -result
}
参考自:http://blog.csdn.net/stu_wanghui/article/details/38564177
3、面向对象思想
设计原则 S.O.L.I.D
简写 | 翻译 |
---|---|
SRP | 单一职责原则 |
OCP | 开放封闭原则 |
LSP | 里氏替换原则 |
ISP | 接口分离原则 |
DIP | 依赖倒置原则 |
单一职责原则
修改一个类的原因应该只有一个
换句话说就是让一个类只负责一件事,当这个类需要做过多的事情的时候,就需要分解这个类。
如果一个类承担的职责过多,就等于把这些职责耦合在了一起,一个职责的变化可能会削弱这个类完成其它职责的能力。
开放封闭原则
类应该对扩展开放,对修改关闭
扩展就是添加新功能的意思,因此该原则要求在添加新功能时不需要修改代码。
符合开闭原则最典型的设计模式是装饰者模式,它可以动态地将职责附加到对象上,而不用去修改类的代码。
里氏替换原则
子类对象必须能够替换掉所有父类对象
继承是一种 IS - A 关系,子类需要能够当成父类来使用,并且需要比父类更特殊。
如果不满足这个原则,那么各个子类的行为上就会有很大差异,增加继承体系的复杂度。
接口分离原则
不应该强迫客户依赖于它们不用的方法
因此使用多个专门的接口比使用单一的总接口更好。
依赖倒置原则
高层模块不应该依赖于低层模块,两者都应该依赖于抽象。
抽象不应该依赖于细节,细节应该依赖于抽象。
高层模块包含一个应用程序中重要的策略选择和业务模块,如果高层模块依赖于低层模块,那么低层模块的改动就会直接影响到高层模块,从而迫使高层模块也需要改动。
依赖于抽象意味着:
- 任何变量都不应该持有一个指向具体类的指针或者引用
- 任何类都不应该从具体类派生
- 任何方法都不应该覆写它的任何基类中的已经实现的方法
4、三大特性
封装
利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分离的独立实体。数据被保护在抽象数据类型的内部,尽可能的隐藏内部细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。
优点:
- 降低耦合:可以独立开发、测试、优化和修改等
- 减轻维护的负担:可以更容易的被程序猿理解,并且在调试的时候可以不影响其他模块
- 有效地调节性能:可以通过剖析确定哪些模块影响了系统的性能
- 提高软件的可重用性
- 降低了构建大型系统的分享:即使整个系统不可用,但是这些独立的模块却有可能是可用的
继承
继承实现了 IS - A 关系,继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。
多态
多态分为编译时多态和运行时多态。编译时多态主要指方法的重载,运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定。
运行时多态有三个条件:
- 继承
- 覆盖
- 向上转型
5、对象拷贝理解?深拷贝、浅拷贝的区别?
首先要先明白为什么需要使用克隆呢?
克隆的对象可能包含一些已经修改过的属性,而 new 出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的 “状态” 就需要克隆了。
那如何实现对象克隆呢?有两种办法:
- 实现 Cloneable 接口并重写 Object 类中的 clone() 方法
- 实现 Serialiable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆
深拷贝和浅拷贝的区别是什么?
-
浅拷贝
当对象被复制的时候只复制它本身和其中包含的值类型的成员变量,而引用类型的成员变量并没有复制。
-
深拷贝
除了对象本身被复制外,对象所包含的所有成员变量也将被复制。
Java 默认的是浅拷贝,如果想实现深拷贝,就需要对象所包含的引用类型的成员变量也需要实现 Cloneable 接口,或者实现 Serialiable 接口。
6、Enumeration 和 Iterator 的区别?
-
接口不同
Enumeration 只能读取集合数据,而不能对数据进行修改;Iterator 除了读取集合的数据之外,也能对数据进行删除操作;Enumeration 已经被 Iterator 取代了,之所以没有被标记为 Deprecated,是因为在一些遗留类(Vector、Hashtable)中还在使用。
public interface Enumeration<E> { boolean hasMoreElements(); E nextElement(); default Iterator<E> asIterator() { return new Iterator<>() { @Override public boolean hasNext() { return hasMoreElements(); } @Override public E next() { return nextElement(); } }; } }
public interface Iterator<E> { boolean hasNext(); E next(); default void remove() { throw new UnsupportedOperationException("remove"); } default void forEachRemaining(Consumer<? super E> action) { Objects.requireNonNull(action); while (hasNext()) action.accept(next()); } }
-
Iterator 支持 fail-fast 机制,而 Enumeration 不支持
Enumeration 是 JDK 1.0 添加的接口。使用到它的函数包括 Vector、Hashtable 等类,Enumeration 存在的目的就是为它们提供遍历接口。
Iterator 是 JDK 1.2 才添加的接口,它是为了 HashMap、ArrayList 等集合提供的遍历接口。Iterator 是支持 fail-fast 机制的。
Fail-fast 机制是指 Java 集合 (Collection) 中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生 fail-fast 事件。例如:当某个线程 A 通过 Iterator 去遍历某集合的过程中,若该集合的内容被其他线程所改变了,那么线程 A 访问集合时,就会抛出 ConcurrentModificationException 异常,产生 fail-fast 事件。
7、哪些情况下的对象会被垃圾回收机制处理掉?
垃圾回收机制中最基本的做法是分代回收。内存区域被划分为三代:新生代、老年代和永久代。对于不同世代可以使用不同的垃圾回收算法。一般来说,一个应用中的大部分对象的存活时间都很短,基于这一点,对于新生代的垃圾回收算法就可以很有针对性。
[Java — 垃圾收集灵魂拷问三连(三)](http://omooo.top/2017/12/27/Java — 关于垃圾收集灵魂拷问三连(三)/#more)
8、讲一下常见的编码方式?
编码的原因:
计算机存储信息的最小单元是一个字节即八个bit,所以能表示的字符范围是0 - 225 个。然而要表示的符号太多了,无法用一个字节来完全表示,要解决这个矛盾必须需要一个新的数据结构char,从char到byte必须编码。编码方式规定了转化的规则,按照这个规则就可以让计算机正确的表示我们的字符。编码方式有以下几种:
ASCII码:
总共有128个,用一个字节的低七位表示,0 - 31是控制字符,如回车键、换行等等。32 - 126是打印字符,可以通过键盘输入并且能够显示出来。
ISO-8859-1:
ISO组织在ASCII码基础上又制定了一些列标准用来扩展ASCII编码,其中IS-8859-1涵盖了大多数西欧语言字符,单字节编码,总共表示256个字节,应用广泛。
GB2312、GBK:
双字节编码,表示汉字。
UTF-16:
UTF - 16具体定义了Unicode字符在计算机中的存取方法。UTF - 16用两个字节来表示Unicode转化格式,这个是定长的表示方法,不论什么字符都可以用两个字节表示,两个字节是十六个bit,所以叫UTF - 16.
UTF- 8:
UTF-16统一采用两个字节表示一个字符,虽然表示上非常简单方便,但是有很大一部分字符用一个字节就可以表示,显然是浪费了存储空间。于是UTF-8应运而生,UTF-8采用一种变长技术,每个编码区域有不同的字码长度,不同类型的字符可以是由1-6个字节组成。
参考自:几种常见的编码格式
9、UTF-8编码中中文占几个字节,int型几个字节?
中文占字节数可以是2、3和4个的,最高为4个字节。
参考自:[http://blog.csdn.net/hellokatewj/article/details/24325653]
10、静态代理和动态代理的区别,什么场景使用?
参考自:https://www.jianshu.com/p/2f518a4a4c2b
方便,但是有很大一部分字符用一个字节就可以表示,显然是浪费了存储空间。于是UTF-8应运而生,UTF-8采用一种变长技术,每个编码区域有不同的字码长度,不同类型的字符可以是由1-6个字节组成。
参考自:几种常见的编码格式