## 1、Java特性:多态
其实一直以来我对多态的理解都不是很清楚。包括定义和实现。`个人认为,多态应该从类的组织层面上来看`
### 多态存在的三个必要条件:
+ 要有继承
+ 要有重写
+ 父类引用指向子类对象
多态其实相对于其他两个Java特性来说字面上不太容易理解,而且也不太容易解释。参照之前实习的面经`中科创达一面面经`中的引用,多态在Java中有三个核心。这里我觉得有必要重新来解读一下:
> 1、多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编译时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
`多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编译时并不确定,而是在程序运行期间才确定`:
其实应该是从类的组织层面去看多态性。下面说一下我自己的看法。
平时的时候我自己写代码经常会有这样的使用:
```java
// base on Java8
Map<String, Integer> map = new HashMap<>();
```
其实这么一种写法已经实现了多态。只是我自己一直没有意识到。
我们先忽略等式的右边,在Map的类组织结构上其实已经满足了多态存在的两个必要条件,一个是继承,一个是方法重写。
而在编写代码的时候也满足了第三个条件:父类引用指向子类对象。这么些会有一个好处就是,我后面的代码都基于Map这个父类进行编写和调用,那么其实整个代码都与具体类型无关了。
那么传入不同的实例对象,根据不同实例对象对应的具体实现类,所进行的动作就会有所不同。这里就是多态的一个好处:`不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变`
> 2、指向子类的父类引用由于向上转型了,它只能访问父类中拥有的方法和属性,而对于子类中存在而父类中不存在的方法,该引用是不能使用的,尽管是重载该方法。若子类重写了父类中的某些方法,在调用该些方法的时候,必定是使用子类中定义的这些方法(动态连接、动态调用)
这里可以引出Java的两种多态机制:
+ 编译时多态:通过方法重载实现
+ 运行时多态:通过方法的覆盖,即父类与子类的优先调用层次。见下一个核心点
> 在继承链中对象方法的调用存在一个优先级:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。`即先查this对象的父类,没有就重头再查参数的父类`
更多的好处可以参考一下这篇在中科创达面经中所引用过的文章,在实习之后其实多态的好处都已经体验过了,只是自己没有意识到而已。
> [Java多态性理解,好处及精典实例](https://blog.csdn.net/jian_yun_rui/article/details/52937791)
## 2、为什么会有并发安全问题
主要还是因为在多个进程并发执行时,对同一个变量的读写可能存在信息不同步、信息不一致的问题。
> [Java内存模型](https://cyc2018.github.io/CS-Notes/#/notes/Java%20%E5%B9%B6%E5%8F%91?id=%e5%8d%81%e3%80%81java-%e5%86%85%e5%ad%98%e6%a8%a1%e5%9e%8b)
在JVM中,具体体现在JVM的内存模型所导致的`缓存一致性`。处理器上的寄存器的读写比内存快好几个数量级,为了解决这个速度矛盾,JVM在他们之间加入了一个高速缓存。这就是线程的工作内存。
所有的变量都存储在主内存中,而工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。
线程只能直接操作工作内存中的变量(这又是线程的一个特点),**不同线程之间的变量值传递需要通过主内存来完成。**
在传递给主存的这个过程或模型中,就是导致信息不一致的关键所在。从主存中读取和写回的时机就会导致并发安全问题
## 3、如何进行线程同步
> [实现线程同步的几种方式总结](https://blog.csdn.net/yoonerloop/article/details/81154596)
在Java中有这么几种方式:
### 通过Object的wait()、notify()和notifyAll()
这是最简单的一个同步方式,基于Object自身的方法,并且只能就同步方法或者同步控制块中使用(带有synchronized关键字)的方法中使用。
wait()会**挂起线程并释放锁**,否则其他线程无法进入对象的同步方法或者同步控制块中执行notify()或notifyAll()方法。
### Condition类的await()、signal()、signalAll()
基于Condition类的同步方法。await()相比wait(),await()可以指定等待条件,因此更加灵活。
Condition要通过Lock实例来获取。
### 通过阻塞队列,如BlockingQueue
Java中BlockingQueue接口有如下几个实现:
+ FIFO队列:LinkedBlockingQueue、ArrayBlockingQueue(固定长度)
+ 优先级队列:PriorityBlockingQueue
通过这个队列可以实现消费者模型
### 通过同步队列,如SynchronousQueue
SynchronousQueue不同于一般的数据等线程,而是线程等待数据,他是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样。
类似于消费者线程要等待生产者线程吧
### 通过Callback回调
当一个线程执行完成后,可以通过Callback回到主线程执行代码。这个方法在安卓中很多地方都有使用。如AsyncTask中的PostExecute就有点类似于Callback结构。
### 通过同步辅助类CountDownLatch和CyclicBarrier
CountDownLatch是一个线程和多个线程之间的同步
CyclicBarrier是多个线程之间的同步
具体可以参考CS-Notes
## 4、Java中如何终止一个线程
Java中提供的方法:
Thread:
+ interrupt()发出中断线程请求
+ stop()强行中断线程,已废弃(包括Thread.stop()/Thread.resume()等)
## 5、String的新建问题
> [String创建对象问题](https://www.cnblogs.com/chenghuanhuaning/p/11255654.html)
Java中有一个**字符串常量池**,是专门存储字符串的一个特定内存区域,且里面的内容都具有唯一性。
创建对象有这么几种情况:
```java
String s = "abc"; // 会在常量池内查找,有则不创建
String s = "a" + "b" + "c"; // 编译器会进行优化,为"abc"
String s = new String("abc") // new关键字会创建一个对象,"abc"会在常量池中查找,不存在即创建,后将创建对象的引用指向这个字符串常量
String s = "abc"; String s1 = s + "ab"; // 底层会产生StringBuilder进行append拼接,最后调用toString输出。这个语句最多会产生三个对象。
String s = "c"; String sq = "a" + "b" + s + "d" + "f"; //会产生五个对象。编译器没有办法识别并优化后两个常量字符合并为一个。则最多会创建:"c"/"ab"/StringBuilder/"d"/"f"
```
## 6、TCP为什么是三次握手?
> [TCP 为什么三次握手而不是两次握手(正解版)](https://blog.csdn.net/lengxiao1993/article/details/82771768)
除了解决可能的资源浪费问题,**还有就是确认序号问题**。我们知道TCP有数据可靠保障机制,其中包含顺序接受、丢失重传等机制。
这三次握手的过程除了确认双方已经做好准备接收数据以外,双方还同时协商确认了接收序列号。
这里通过一个图来快速理解。
![image](https://img-blog.csdn.net/20180919162218818?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xlbmd4aWFvMTk5Mw==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)
另外上面的引用链接里面还有个时序图,通过这两个图结合可以理解TCP三次握手的真正意义:**协商双方接收序列号**
## 7、FTP是基于什么协议的
FTP文件传输协议,是位于应用层的协议,使用TCP而不是UDP。
TFTP(Trivial File Transfer Protocol)简单文件传输协议是基于UDP的。
面试的时候再回答这个问题还提到了SocketServer编程。
### 8、什么是socket?
> [Socket的学习(一)什么是Socket?](https://blog.csdn.net/weixin_39258979/article/details/80835555)
socket,套接字,是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。
传输层实现端到端的通信,这个“端”就是套接字。传输层链接的断点叫做套接字。
### TCP/IP五层模型
#### 应用层
为计算机用户提供应用接口,也为用户直接提供各种网络服务。应用层协议包括HTTP/HTTPS/FTP/POP3/SMTP
#### 传输层
定义传输数据的协议端口号,流量控制和差错校验。数据包一旦离开网卡即进入网络传输层。
#### 网络层
进行逻辑地址寻址,实现不同网络之间的路径选择。IP则是在这一层上
#### 数据链路层
建立逻辑连接、进行硬件地址寻址、差错校验等。
#### 物理层
建立、维护、断开物理连接,一般是指两个交换机或路由器的连接。
## 9、聊一聊匿名类
说匿名类之前应该先说一下内部类。
内部类有以下几种:
### 成员内部类
成员内部类是最普通的内部类,它的定义为位于另一个类的内部。
非静态内部成员类会对外部类隐式持有一个引用。所以有可能造成内存泄漏的现象。
成员内部类可以无条件访问外部类的成员。而外部类想访问成员内部类就不那么容易了。外部类必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问。
非静态成员内部类是依附外部类存在的,也就是说如果要创建一个成员内部类的对象,必须存在一个外部类对象。**其实成员内部类的地位和类中的其他成员(Field)地位是相同的**
成员内部类一样可以拥有访问权限修饰符。但是值得注意的是,当内部类的成员的访问修饰符为private时,外部类一样可以访问到这个成员。具体地,编译器其实是给内部类添加了一个access函数,使得外部类可以直接通过成员访问符号`.`来访问内部类的private成员。
### 局部内部类
局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问只能在方法中或该作用域内。
### 匿名内部类
匿名类其实是一个特殊的内部类,因为很多时候这个类只要用到一次,比如在安卓中针对不同的Button需要设置不同的OnClickListener,因为很多时候不存在复用的问题,所以匿名类在这里是非常合适的,因为名称在这里并没有什么用处。
---
非静态内部类想要访问外部类,则应该如此调用:`Outer.this`
## 10、安卓的内部非静态Handler类为什么会产生内存泄漏
一般的Handler在作为内部类时,Handler的生命周期其实是不受控制的。
而在GC的可达性算法中,如果有任何一个GC Root对象能够到达当前的对象,那么这个对象就不会被回收。
在进一步,Handler一般都是我们在Activity等组件中的OnCreate()等方法中使用new显式创建的。而正好用new创建的对象就是GCRoot对象。
同时上面对内部类的解释也提到了,非静态内部类的实例会隐式持有外部类的引用,故这里的Handler对外部的Activity就持有一个引用了。
所以当Handler存在与类似于Activity、Service这种生命周期有明确控制的组件中,如果这些组件的生命周期走完了,意味着这些组件可以被回收了。但是又因为之前显式new创建了一个Handler,对外部组件存在一个引用,**那么这些走完生命周期的组件无法被GC回收,进而造成了内存泄漏。**
解决方法很简单,就是将Handler修饰为static的,并持有一个外部类的**弱引用**。这样就不会妨碍内部类的回收了。
> 这里我自己又想到了一个问题,那么Handler自己怎么被回收呢?
【秋招面经】腾讯音乐-秋招提前批-安卓开发工程师-一面总结