感觉这次凉凉啊
在开始的时候要求自我介绍,这是第一次呢
感觉半分钟没有就讲完了,这里来总结一下要讲什么吧
## NaN、个人介绍
+ 个人基本情况
+ 在学校任职,做的事情
+ 会什么语言、常用的开发语言
+ 做过什么项目、哪几个项目是比较有挑战的
+ 正在进行什么开发
+ 喜欢哪方面的开发
+ 参加实习的目的
+ 未来的规划
+ 自己对自己的评价
## 0、设计模式之六大原则
如果想要很好地了解IOC的思想,那么我觉得了解设计模式是必不可少的。今天现在这里讨论一下设计模式的六大原则。
> 参考: [设计模式之六大原则(转载)](https://www.cnblogs.com/dolphin0520/p/3919839.html)
### 单一职责原则 Single Responsibility Principle SRP
一个类所负担的方法不能太多。一个类的职责越多,他被复用的可能性就越小。**并且一个类的职责过多,就相当于把职责耦合到一起。** 当一个职责变化时,可能会影响其他的职责的运作。
因此,把职责分离,**其实也是一种解耦。**
==单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又最难运用的原则。== 需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。
内聚性:又称块内联系。指模块的功能强度的度量,**即一个模块内部各个元素彼此结合的紧密程度的度量。** 若一个模块内各元素(语名之间、程序段之间)联系的越紧密,则它的内聚性就越高。
耦合性:也称块间联系。指软件系统结构中各模块间相互联系紧密程度的一种度量。**模块之间联系越紧密,其耦合性就越强,模块的独立性则越差。** 模块间耦合高低取决于模块间接口的复杂性、调用的方式及传递的信息
### 开闭原则 Open-Closed Principle OCP
**一个软件实体应当对==扩展开放,对修改关闭==。即软件实体应尽量在不修改原有代码的情况下进行扩展。**
这条规则限定了我们应该在最大程度上限定已经开发好的类的修改。在考虑扩展功能的时候,首先要考虑的通过扩展原来的类,编写新的类去增加新的功能,而不是考虑去类的内部修改代码
软体都会面临一个问题就是,他们的需求会随着时间的推移发生变化。当软件系统面对新的需求的时候,我们**应该尽量保证系统的设计框架是稳定的**(既有的代码结构和内容是稳定的)。
当一个软体符合OCP,那么他就会拥有良好的适应性和灵活性,同时具备较好的稳定性和延续性。
#### 如何实现OCP?
**对系统进行抽象化设计**,是开闭原则的关键所在。可以为系统定义一个相对稳定的抽象层(如抽象类、接口等),而将不同的实现行为移至具体的实现层完成。
当我们需要扩展业务功能时,无需对抽象层进行任何改动,只需要增加新的具体类来实现业务功能即可,而不需要对已有的代码进行改动。
### 里氏代换原则 Liskov Substitution Principle LSP
**所有引用基类(父类)的地方必须能够透明地使用其子类的对象,反之则不成立**
**通俗来讲,就是子类可以扩展父类的功能,但不能改变父类原有的功能**
在Java里编译器会检查子类向父类的因式转换(向上升级),所以Java实现了一部分的里氏代换原则
**在软件中将一个基类对象替换成他的子类对象,程序将不会产生任何错误和异常。反过来则不成立。**
这一句话隐含了一个意思,就是在继承的时候遵循里氏代换原则。**除了添加新的方法完成新的业务之外,不要重写或者重载父类的方法。**
意思就是,**在父类中凡是实现好的方法,实际上是设定一系列的规范和规约。** 虽然它不强制要求所有子类必须遵循这些规约,但是如果子类对这些非抽象的方法任意修改,就会对整个继承体系造成破坏。里氏代换原则就是表达了这个含义
如果子类重写或者重载了父类的方法,那么就==做不到==LSP中所说的引用基类的地方==能够透明使用==其子类,因为父类的方法已经被修改,规约被破坏,所以在使用父类的地方如果使用子类就有可能造成因为方法的实现过程不同而导致表现的不同。
### 依赖倒转原则 Dependency Inversion Principle DIP
依赖倒转就是OOP的主要实现机制之一
**DIP要求我们在代码中传递参数时或者在关联关系中,尽量引用层次高的抽象类,即使用接口和抽象类进行各种各样的声明,而不要用具体类来做这些事情。**
**抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。**
既然是依赖倒转,那么我们应该先弄明白原来的依赖是怎么样的。
> 引用: [轻松学,浅析依赖倒置(DIP)、控制反转(IOC)和依赖注入(DI)](https://blog.csdn.net/briblue/article/details/75093382)
```
public class Person {
private Bike mBike; //具体实现类
public Person() {
mBike = new Bike();
}
public void chumen() {
System.out.println("出门了");
mBike.drive();
}
}
```
在这里是个典型的依赖。在我们需要某个类的时候,会在类的内部主动创建一个对象去依赖他。这个**主动的创建其实就是一个高耦合的表现**。
那么有没有方法能够使需求在变动的时候(例如我想使用飞机或者火车出行,甚至步行),能够不更改Person类内部的代码?
首先我们看到,上面人的出行能力依赖于Bike的能力。这其实就是高层依赖低层的典型例子。也就是说,我们这么编程其实是面向实现编程的(事实上我们也没有引进任何的抽象)。这样造成的结果就是**Person的泛化性不足,在出现需求变动的时候无法很好地适应。**
那么如果针对抽象编程呢?
```
public class Person {
private Driveable mDriveable; //是接口,不是具体实现类
public Person() {
mDriveable = new Train();
}
public void chumen() {
System.out.println("出门了");
mDriveable.drive();
}
}
```
现在的Person的出门方法依赖于Driveable接口的抽象,他没有限定自己出门的可能性,任何继承并实现了Driveable的实现类都是可以的。
在这里实现了细节依赖于抽象。就是人出行的这个细节,依赖于Driveable这个抽象接口。那么什么叫抽象依赖于细节呢?
我的认为是,当具体的实现类设计出来之后,再将他的方法抽象出来,这是抽象依赖于抽象。抽象出来的接口并不具有很强的泛化性。而先抽象出接口再去进行具体实现,这个抽象就会具有比较强的泛化性。
那么这个依赖倒转的倒转是什么意思呢?
原作者说到,这个倒转其实是改变依赖关系。高层模块不应该依赖于底层模块,而是两者都依赖于抽象。
那么这里倒转在哪里呢?
其实是这样的。**这个接口的产生是来自于上层类对他的需求的一个抽象。**
在我们刚刚的例子里面,我们的人需要出行。人把出行这个行为抽象成接口,然后底层去实现。
这里的倒转的具体含义就出来了————不是高层依赖于底层的实现,而是高层抽象自己的需求,两者共同依赖于抽象(这个抽象其实也是属于高层的)
那么这里引入我们的大BOSS,我一直搞不懂的——控制翻转IOC与依赖注入,就恰到好处了
#### 1、控制反转IOC和依赖注入DI————OCP和DIP的具体实现
刚刚我们在上面实现了依赖转置,解决了软体泛化性不强的问题。但是因为实现类仍然是在类内部创建的,一旦我对人出行的需求需要更改,比如我要把出行方式从火车改成飞机,那么仍然要进到类的内部更改代码,不符合开闭原则OCP。
而控制翻转就是来解决违反OCP的情况的。控制反转是一种思想,一个重要的面向对象变成的法则。他能够让我们设计出松耦合,更加优良的程序。
与DIP不同的地方时,DIP强调对于传统的、源于面向过程设计软体思想的层次概念的“倒置”,而IOC则是强调对程序流程控制权的反转。IOC强调将传统设计上在类内创建的对象交由调用我这个类的外部来创建,然后通过依赖注入的方式放到当前类的内部,最终实现对依赖类的使用。
在传统的应用程序中,在我们需要一个类的时候,我们都是在现在的**这个类的内部主动地去创建我们依赖的对象,** 从而导致对象之间的高耦合。
再举个栗子叭
某天,公司领导找到开发人员,说要开发一个微信支付宝的收款明细获取功能。
案例精简:把任务指派给开发人员完成。本句话中,有两个名词:“任务”和“开发人员”,所以我们考虑设计两个对象(任务和开发人员)。
开发人员对象:
```
package DependencyInjectionDemo;
public class Javaer {
private String name;
public Javaer(String name) {
this.name = name;
}
public void setName(String name) {
this.name = name;
}
public void WriteCode() {
System.out.println(this.name + " writting java code...");
}
}
```
任务对象:
```
package DependencyInjectionDemo;
public class NewTask {
private String name;
private Javaer javaer;
public NewTask(String name) {
this.name = name;
this.javaer = new Javaer("张三"); //主动创建要依赖的对象
}
public void Start() {
System.out.println(this.name + " started ..");
this.javaer.WriteCode();
}
}
```
场景类
```
package DependencyInjectionDemo;
public class DependencyInjectionDemo {
public static void main(String[] args) {
NewTask task = new NewTask("开发微信支付宝收款明细获取工具");
task.Start();
}
}
```
上面这是一个很典型的Java SE设计。我们来看看他有什么问题
+ 当如果我们需要的开发人员不是张三了,而是李四了,那么我们就要进到代码内部去修改代码
+ 如果有人需要复用我们的实现,我们不能直接打包成jar直接给他们用,因为他不能从jar文件外部修改任务和开发人员。
上面两点造成的结果就是,复用性不强,鲁棒性不强。一旦当需求出现了更改(例如需要更换任务或者更换开发人员或者更换人员种类)那么这个代码就没法用了
---
那么怎么样解决上面的问题呢?
这里就是依赖注入出场的时候了。我们不要让依赖的类在类的内部主动生成,而是通过**构造器或者Setter或者接口让外部环境去给他一个类**,这就是控制反转的核心:需要的时候不要再类的内部主动创建其他的类,而是在类的外部创建类,然后由外部通过方法注入类内,最终实现依赖。**让主动创建变成被动接受依赖。** 同时通过这种方法也实现了解耦。看下面的例子
任务对象;
```
package DependencyInjectionDemo;
public class NewTask {
private String name;
private Javaer javaer;
public NewTask(String name) {
this.name = name;
//this.javaer = new Javaer("张三");
}
public void SetJavaer(Javaer javaer) {
this.Javaer = javaer;
}
public void Start() {
System.out.println(this.name + " started ..");
this.javaer.WriteCode();
}
}
```
场景类
```
package DependencyInjectionDemo;
public class DependencyInjectionDemo {
public static void main(String[] args) {
NewTask task = new NewTask("开发微信支付宝收款明细获取工具");
task.SetJavaer(new Javaer("张三")); //构造方法中进行依赖注入
task.Start();
}
}
```
上面的例子就是一个基于依赖注入DI的控制反转IOC。
在这里高层不再依赖于底层了,因为在类内没有代码显式依赖低层。
上层对下层类的控制体现在哪呢?主要体现在上层现在可以决定从什么地方进行注入。依赖注入有三种,分别是构造器注入(要求注入的类的生命周期较长),Setter注入和接口注入。
同时,对反转有另外一种的理解就是,现在类不是主动创建依赖的类了,而是交由外部创建,然后我被动地接受我需要依赖的类。
基于这么一个思想,一个专门分析和创建类的实例、管理类的生命周期的框架就显得很必要了。那么,这样的一个框架就叫做IOC容器。
这里,场景类也作为一个IOC容器,负责创建类并且注入不同的类。
在Spring里,是采用反射的方式根据注解或者xml配置文件来生成类的。这是对工厂模式的一种升华,采用反射去生成类与工厂模式相比,在更改或者扩展业务的时候,不需要进到工厂类中更改相应判断不同类或者接口的代码,这进一步保证了OCP。
可以参考下面的一些文献便于理解:
> [理解依赖注入和控制反转](https://www.cnblogs.com/zanpen2000/p/7810884.html)
> [Spring IoC有什么好处呢?](https://www.zhihu.com/question/23277575)
### 接口隔离原则 Interface Segregation Principle ISP
**使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。**
每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。
这里的接口有两种不同的含义:
1. 一类所具有的方法的特征的集合,仅仅是逻辑上的一种抽象。
2. 语言中具体的接口(Interface)的定义,有严格的定义和结构。
ISP对不同的含义的定义也不同:
1. 对接口的划分直接带来类型的划分。可以把接口理解成角色,一个接口代表一个角色,每个角色都有和他直接相关的接口。这里ISP也可以叫做“角色隔离原则”
2. 接口仅仅提供客户想要的行为(例如上面DIP所定义的下层接口),客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的、单独的接口,而不要提供大的接口。
接口应该尽量细化,同时接口中的方法应该尽量少,每个接口中只包含一个客户端(如子模块或业务逻辑类)所需的方法即可,这种机制也称为“定制服务”,即为不同的客户端提供宽窄不同的接口。
具体地,如果在java设计中接口过大,在扩展的过程中发现我不需要的但是又在接口中定义了的方法。我这个类继承了接口,就一定要实现这些方法。**所以最终可能会导致我对我不需要的方法定义了空的代码段,造成代码上的冗余。**
同时,在设计接口的时候还是要控制接口的粒度。太大在扩展的时候可能会带来代码的冗余,接口太小会造成接口的泛滥,难以维护。
### 迪米特法则 Law Of Demetter LOD
**一个软件实体应当尽可能少地与其他实体发生相互作用。**
迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。
图灵杯中设计的事件驱动模式,是符合OCP、LOD、ISP的。
## 2、面向切面编程 AOP
spring框架中,显著的特点除了IOC、DI之外,就是AOP了。那么什么是AOP呢?
AOP,面向切面编程,是Spring框架中一个重要的内容,是函数式编程的一种衍生泛型。
AOP利用一种“横切”的技术,能够剖开封装的对象内部,并且将那些影响了多个类并且和具体业务无关的公共行为,封装成一个独立的模块
例如之前在SpringData内使用的审计模块,自动为数据库实体类添加创造和更改时间。
又比如很多的权限审核、日志记录、性能统计。安全控制、事务处理、异常处理等等,这些很多类需要用到的,但是实际上和核心业务逻辑没有什么关系的代码,都可以通过AOP横切出来,单独组织成切面类。
更重要的是,他又能巧妙地将这些剖开的切面复原,不留痕迹地融入核心业务逻辑当中。
AOP技术的实现,无非就是通过动态代理技术、或者在程序编译期间进行静态地“织入”方式。
### AOP和OOP的区别?
OOP:针对业务处理过程中的实体及其属性和行为进行抽象封装,以获得更加清晰、高效的逻辑单元划分。
AOP:针对业务处理过程中的某个具体过程——切面进行提取,他面对的是业务逻辑中的步骤或者片段,提取出公共的步骤和片段,从而达到降低各逻辑之间降低耦合的隔离效果。
对于“雇员”这样业务实体的封装,自然是OOP的任务,AOP的设计思想对“雇员”的封装无从谈起。
而对于“权限检查”这样的动作片段进行划分,则是AOP的目标领域。使用OOP对一个动作进行封装,显得有点不伦不类。
## 3、HashMap实现原理
直接说HashMap类的内部构造吧。结合源码和文章一起理解
> 参考文章: [HashMap实现原理及源码分析](https://www.cnblogs.com/chengxiao/p/6059914.html)
首先,内部维护了一个用于散列的数组,主干长度一定的2的次幂(为什么?)。
```
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
```
而Map中实现Entry<K, V>的实现类Node是一个单链表结构
```java
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //哈希值,避免重复计算
final K key; //键,不可更改
V value;
Node<K,V> next; //指向链表的后继,如果没有则为null,表示没有冲突,不存在链表
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
```
简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的
如果定位到的数组元素不存在链表,则直接返回第一个结果,复杂度为常数阶O(1)
如果定位到的数组元素存在链表,那么就要通过查询到的Node中实现了Entry的equals方法进行比对。这个复杂度是线性阶O(n)
在Map的常规构造器中,没有直接为散列表table分配空间。而是在执行put操作的时候才真正构建数组。有点像懒加载。
### 为何HashMap的数组长度一定是2的次幂?
由于散列函数是h & (lenth - 1),2的次幂保证了低位都是1,这有两点好处:
1. 新的散列表和旧的散列表相比,他们的差异是最高位的那个1(如16时,length-1为01111,32时为011111),所以如果将最高位上的那个差异位置0,那么新数组和旧数组的索引就完全一致了
2. 保持2的次幂,可以保证length - 1的低位全部都为1,进而使得获得的新的数组索引index更加均匀。因为Map设计的哈希函数就已经尽可能散列低位了。
### 重写equals方法同时要重写hashCode方法
如果不同时重写,在使用和哈希表有关的类的时候可能带来问题
下面的说明中,hashCode方法为Object最原始的方法
HashMap的put方法的顺序为:
> key.hashCode()-->hash-->indexFor-->最终索引位置
get方法为:
> key.hashCode()-->hash-->indexFor-->最终索引位置
由于没有重写hashCode方法,只要传入的两个key不是指向同一个对象的话,hashCode就可能不同,进而导致搜索到错误的数组元素上。
### hashMap可以用于并发吗?
不可以。
在并发的时候会出现死循环,从而导致调用get()方法时CPU使用率100%的问题
主要是由于resize()中重新构造链表的方法可能在多线程中产生环形链表。
## 4、ConcurrentHashMap是怎么实现并发安全的?
hashTable使用同步锁Synchronized,效率低下
SDK 1.7中,ConcurrentHashMap采用分段锁技术提高访问效率
其中维护一个Segement数组,一个Segement元素存储的是是HashEntry数组+链表,这个结构和HashMap的存储结构一样。只不过不一定是一个HashMap桶对应一把锁,而有可能是多个桶对应一把锁
SDK1.8中直接用Node数组+链表+红黑树的数据结构来实现。并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap
> 参考: [高并发编程系列:ConcurrentHashMap的实现原理(JDK1.7和JDK1.8)](https://baijiahao.baidu.com/s?id=1617089947709260129&wfr=spider&for=pc)
![image](https://ss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=1897711906,4275469846&fm=173&app=25&f=JPEG?w=640&h=316&s=4CAE38728482D6A242FC58C600007022)
左边为HashMap,右边为转换成红黑树的ConcurrentHashMap,查找性能优化为O(logN)
在1.8中,锁是针对一棵树来讲的。所以并发能力比Segement更高
比较:
1. 数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
2. 保证线程安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。
3. 锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)。
4. 链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。
5. 查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(logN)。
### 拓展:AQS和CAS
> 参考: [JAVA并发编程: CAS和AQS](https://blog.csdn.net/u010862794/article/details/72892300)
#### CAS
Compare And Swap,即比较并交换。是解决多线程并行情况下使用锁造成性能损耗的一种机制,属于一种乐观锁。
CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。
无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。
#### AQS
Abstract Queued Synchronizer,抽象队列式同步器,是JDK下提供的一套用于实现基于FIFO等待队列的阻塞锁和相关的同步器的一个同步框架。
> 参考: [AQS深入理解与实战----基于JDK1.8](https://www.cnblogs.com/awakedreaming/p/9510021.html)
想弄懂AQS,先要弄懂“同步器”的概念:
同步器是一种抽象数据类型,在该类型的内部,维护了以下内容:
1. 一个状态变量,该变量的不同取值可以表征不同的同步状态语义(例如表示一个锁已经被线程持有了还是没有任何线程持有)
2. 能够更新和检查该状态变量值的操作(方法)集合
3. 至少有一个方法——当同步状态的值需要时可调用该方法阻塞来修改该状态的线程;或当其他的线程修改了同步状态值,可允许调用该方法唤醒其他阻塞线程
总结来说,就是:同步器中包含一个**可表征同步状态的变量**,**可操作该变量的方法集**,**以及可阻塞或唤醒其他来修改该状态的线程的方法集**。
“抽象”是说该类是一个抽象类,“队列式同步器”是说AQS使用队列来管理多个抢占资源的线程。
AQS在其内部实现了上面所说的同步器的三要素,而且它会把抢占资源失败的线程放入自己内部的一个队列当中维护起来,在这个队列内部的线程会排队等待获取线程。
当多个线程同时调用它的lock()方法获取锁时,它们的本质操作其实就是将该锁实例的同步状态变量的值由0修改为1。第1个抢到这个操作执行的线程就成功获取了锁,后续执行操作的线程就会看到状态变量的值已经为1了,即表明该锁已经被其他线程获取,它们抢占锁失败了。这些抢占锁失败的线程会被AQS放入到一个队列里面去维护起来。当然,实际的情况肯定要稍微复杂些,但本质上是这个道理。
## 5、TCP如何保证连接的可靠性
七大方法:
+ 校验和(保证数据正确)
+ 序列号(保证数据按顺序交付)
+ 确认应答(保证数据传输到达)
+ 超时重传(同上)
+ 连接管理(三次握手四次挥手,保证连接可靠)
+ 流量控制(双方带宽或者处理速度不一致时保证数据完整性)
+ 拥塞控制(保证网络传输可靠)
## 6、线程池的种类
1. CachedThreadPool(可缓存线程池):如果线程池长度超过处理需要,可以回收线程。反之,则创建线程。几乎没有最大线程数限制。如果一个线程空闲了指定时间(默认为1分钟),则工作线程自动终止。
2. FixedThreadPool(固定线程数线程池):创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。**注意:在一开始的时候线程的数量并不一定达到最大,而是在提交任务的过程中不断创建线程并达到指定值。整个过程不会回收线程**
3. SingleThreadExecutor(单线程化执行器):只创建并使用唯一的工作线程来执行任务。当这个工作线程异常结束,会有另外一个工作线程被创建来取代他
4. ScheduleThreadPool(定长线程池):支持定时的、延时的或者周期性的任务执行
5. ForkJoinPool
6. WorkStealingPool
## 7、使用线程池的注意事项
+ 注意预防死锁,特别是有些是因为线程池而造成的死锁
+ 建议使用new ThreadPoolExecutor(...)的方式创建线程池
+ 合理设置线程数
+ 设置能够代表具体业务的线程名称
+ 注意线程泄漏
+ 注意任务过载
<br />
<br />
<br />
<br />
<div align="right">
Chen Sicong
搬运时间:2019年8月2日 22:21:50
</div>
【实习面经】阿里 一面总结