接着上篇继续更新.
- /* 请尊重作者劳动成果, 转载请标明原文链接:*/
- /* https://www.cnblogs.com/jpcflyer/p/10808649.html */
题目一: 接口和抽象类有什么区别?
一般回答:
接口是对行为的抽象, 它是抽象方法的集合, 利用接口可以达到 API 定义和实现分离的目的. 接口, 不能实例化; 不能包含任何非常量成员, 任何 field 都是隐含着 public static final 的意义; 同时, 没有非静态方法实现, 也就是说要么是抽象方法, 要么是静态方法. Java 标准类库中, 定义了非常多的接口, 比如 java.util.List.
抽象类是不能实例化的类, 用 abstract 关键字修饰 class, 其目的主要是代码重用. 除了不能实例化, 形式上和一般的 Java 类并没有太大区别, 可以有一个或者多个抽象方法, 也可以没有抽象方法. 抽象类大多用于抽取相关 Java 类的共用方法实现或者是共同成员变量, 然后通过继承的方式达到代码复用的目的. Java 标准库中, 比如 collection 框架, 很多通用部分就被抽取成为抽象类, 例如 java.util.AbstractList.
Java 类实现 interface 使用 implements 关键词, 继承 abstract class 则是使用 extends 关键词, 我们可以参考 Java 标准库中的 ArrayList.
下面再给出一种扩展回答:
1. 语法层面上的区别
1)抽象类可以提供成员方法的实现细节, 而接口中只能存在 public abstract 方法;
2)抽象类中的成员变量可以是各种类型的, 而接口中的成员变量只能是 public static final 类型的;
3)一个类只能继承一个抽象类, 而一个类却可以实现多个接口.
2. 设计层面上的区别
1)抽象类是对一种事物的抽象, 即对类抽象, 而接口是对行为的抽象. 抽象类是对整个类整体进行抽象, 包括属性, 行为, 但是接口却是对类局部 (行为) 进行抽象. 举个简单的例子, 飞机和鸟是不同类的事物, 但是它们都有一个共性, 就是都会飞. 那么在设计的时候, 可以将飞机设计为一个类 Airplane, 将鸟设计为一个类 Bird, 但是不能将 飞行 这个特性也设计为类, 因此它只是一个行为特性, 并不是对一类事物的抽象描述. 此时可以将 飞行 设计为一个接口 Fly, 包含方法 fly( ), 然后 Airplane 和 Bird 分别根据自己的需要实现 Fly 这个接口. 然后至于有不同种类的飞机, 比如战斗机, 民用飞机等直接继承 Airplane 即可, 对于鸟也是类似的, 不同种类的鸟直接继承 Bird 类即可. 从这里可以看出, 继承是一个 "是不是" 的关系, 而 接口 实现则是 "有没有" 的关系. 如果一个类继承了某个抽象类, 则子类必定是抽象类的种类, 而接口实现则是有没有, 具备不具备的关系, 比如鸟是否能飞(或者是否具备飞行这个特点), 能飞行则可以实现这个接口, 不能飞行就不实现这个接口.
2)设计层面不同, 抽象类作为很多子类的父类, 它是一种模板式设计. 而接口是一种行为规范, 它是一种辐射式设计. 什么是模板式设计? 最简单例子, 大家都用过 ppt 里面的模板, 如果用模板 A 设计了 ppt B 和 ppt C,ppt B 和 ppt C 公共的部分就是模板 A 了, 如果它们的公共部分需要改动, 则只需要改动模板 A 就可以了, 不需要重新对 ppt B 和 ppt C 进行改动. 而辐射式设计, 比如某个电梯都装了某种报警器, 一旦要更新报警器, 就必须全部更新. 也就是说对于抽象类, 如果需要添加新的方法, 可以直接在抽象类中添加具体的实现, 子类可以不进行变更; 而对于接口则不行, 如果接口进行了变更, 则所有实现这个接口的类都必须进行相应的改动.
下面看一个网上流传最广泛的例子: 门和警报的例子: 门都有 open( )和 close( )两个动作, 此时我们可以定义通过抽象类和接口来定义这个抽象概念:
- abstract class Door {
- public abstract void open();
- public abstract void close();
- }
或者:
- interface Door {
- public abstract void open();
- public abstract void close();
- }
但是现在如果我们需要门具有报警 alarm( )的功能, 那么该如何实现? 下面提供两种思路:
1)将这三个功能都放在抽象类里面, 但是这样一来所有继承于这个抽象类的子类都具备了报警功能, 但是有的门并不一定具备报警功能;
2)将这三个功能都放在接口里面, 需要用到报警功能的类就需要实现这个接口中的 open( )和 close( ), 也许这个类根本就不具备 open( )和 close( )这两个功能, 比如火灾报警器.
从这里可以看出, Door 的 open() ,close()和 alarm()根本就属于两个不同范畴内的行为, open()和 close()属于门本身固有的行为特性, 而 alarm()属于延伸的附加行为. 因此最好的解决办法是单独将报警设计为一个接口, 包含 alarm()行为, Door 设计为单独的一个抽象类, 包含 open 和 close 两种行为. 再设计一个报警门继承 Door 类和实现 Alarm 接口.
- interface Alram {
- void alarm();
- }
- abstract class Door {
- void open();
- void close();
- }
- class AlarmDoor extends Door implements Alarm {
- void oepn() {
- //....
- }
- void close() {
- //....
- }
- void alarm() {
- //....
- }
- }
题目二: 如何保证集合是线程安全的?
一般回答:
Java 提供了不同层面的线程安全支持. 在传统集合框架内部, 除了 Hashtable 等同步容器, 还提供了所谓的同步包装器(Synchronized Wrapper), 我们可以调用 Collections 工具类提供的包装方法, 来获取一个同步的包装容器(如 Collections.synchronizedMap), 但是它们都是利用非常粗粒度的同步方式, 在高并发情况下, 性能比较低下.
另外, 更加普遍的选择是利用并发包提供的线程安全容器类, 它提供了:
各种并发容器, 比如 ConcurrentHashMap,CopyOnWriteArrayList.
各种线程安全队列(Queue/Deque), 如 ArrayBlockingQueue,SynchronousQueue.
各种有序容器的线程安全版本等.
具体保证线程安全的方式, 包括有从简单的 synchronize 方式, 到基于更加精细化的, 比如基于分离锁实现的 ConcurrentHashMap 等并发实现等. 具体选择要看开发的场景需求, 总体来说, 并发包内提供的容器通用场景, 远优于早期的简单同步实现.
接下来继续扩展下 HashMap 和 ConcurrentHashMap 的实现. 因为 JAVA7 和 JAVA8 的实现区别很大, 下面分别从 JAVA7 和 JAVA8 简单介绍下其实现.
Java7 HashMap
HashMap 是最简单的, 一来我们非常熟悉, 二来就是它不支持并发操作, 所以源码也非常简单.
首先, 我们用下面这张图来介绍 HashMap 的结构.
大方向上, HashMap 里面是一个数组, 然后数组中每个元素是一个单向链表.
上图中, 每个绿色的实体是嵌套类 Entry 的实例, Entry 包含四个属性: key, value, hash 值和用于单向链表的 next.
Java7 ConcurrentHashMap
ConcurrentHashMap 和 HashMap 思路是差不多的, 但是因为它支持并发操作, 所以要复杂一些.
整个 ConcurrentHashMap 由一个个 Segment 组成, Segment 代表 "部分" 或 "一段" 的意思, 所以很多地方都会将其描述为分段锁. 注意, 行文中, 我很多地方用了 "槽" 来代表一个 segment.
简单理解就是, ConcurrentHashMap 是一个 Segment 数组, Segment 通过继承 ReentrantLock 来进行加锁, 所以每次需要加锁的操作锁住的是一个 segment, 这样只要保证每个 Segment 是线程安全的, 也就实现了全局的线程安全.
concurrencyLevel: 并行级别, 并发数, Segment 数, 怎么翻译不重要, 理解它. 默认是 16, 也就是说 ConcurrentHashMap 有 16 个 Segments, 所以理论上, 这个时候, 最多可以同时支持 16 个线程并发写, 只要它们的操作分别分布在不同的 Segment 上. 这个值可以在初始化的时候设置为其他值, 但是一旦初始化以后, 它是不可以扩容的.
再具体到每个 Segment 内部, 其实每个 Segment 很像之前介绍的 HashMap, 不过它要保证线程安全, 所以处理起来要麻烦些.
Java8 HashMap
Java8 对 HashMap 进行了一些修改, 最大的不同就是利用了红黑树, 所以其由 数组 + 链表 + 红黑树 组成.
根据 Java7 HashMap 的介绍, 我们知道, 查找的时候, 根据 hash 值我们能够快速定位到数组的具体下标, 但是之后的话, 需要顺着链表一个个比较下去才能找到我们需要的, 时间复杂度取决于链表的长度, 为 O(n).
为了降低这部分的开销, 在 Java8 中, 当链表中的元素超过了 8 个以后, 会将链表转换为红黑树, 在这些位置进行查找的时候可以降低时间复杂度为 O(logN).
来一张图简单示意一下吧:
注意, 上图是示意图, 主要是描述结构, 不会达到这个状态的, 因为这么多数据的时候早就扩容了.
下面, 我们还是用代码来介绍吧, 个人感觉, Java8 的源码可读性要差一些, 不过精简一些.
Java7 中使用 Entry 来代表每个 HashMap 中的数据节点, Java8 中使用 Node, 基本没有区别, 都是 key,value,hash 和 next 这四个属性, 不过, Node 只能用于链表的情况, 红黑树的情况需要使用 TreeNode.
我们根据数组元素中, 第一个节点数据类型是 Node 还是 TreeNode 来判断该位置下是链表还是红黑树的.
Java8 ConcurrentHashMap
Java7 中实现的 ConcurrentHashMap 说实话还是比较复杂的, Java8 对 ConcurrentHashMap 进行了比较大的改动. 建议读者可以参考 Java8 中 HashMap 相对于 Java7 HashMap 的改动, 对于 ConcurrentHashMap,Java8 也引入了红黑树.
说实话, Java8 ConcurrentHashMap 源码真心不简单, 最难的在于扩容, 数据迁移操作不容易看懂.
我们先用一个示意图来描述下其结构:
结构上和 Java8 的 HashMap 基本上一样, 不过它要保证线程安全性, 所以在源码上确实要复杂一些.
题目三: 谈谈你知道的设计模式?
大致按照模式的应用目标分类, 设计模式可以分为创建型模式, 结构型模式和行为型模式.
创建型模式, 是对对象创建过程的各种问题和解决方案的总结, 包括各种工厂模式(Factory,Abstract Factory), 单例模式(Singleton), 构建器模式(Builder), 原型模式(ProtoType).
结构型模式, 是针对软件设计结构的总结, 关注于类, 对象继承, 组合方式的实践经验. 常见的结构型模式, 包括桥接模式 (Bridge), 适配器模式(Adapter), 装饰者模式(Decorator), 代理模式(Proxy), 组合模式(Composite), 外观模式(Facade), 享元模式(Flyweight) 等.
行为型模式, 是从类或对象之间交互, 职责划分等角度总结的模式. 比较常见的行为型模式有策略模式(Strategy), 解释器模式(Interpreter), 命令模式(Command), 观察者模式(Observer), 迭代器模式(Iterator), 模板方法模式(Template Method), 访问者模式(Visitor).
关于设计模式就不再扩展了, 童鞋们感兴趣的话, 可以对每种设计模式的使用场景进行思考总结.
来源: https://www.cnblogs.com/jpcflyer/p/10808649.html