一、JAVA基本数据类型的基本操作
Math类相关函数
(1)double floor(double a):表示向下取整,返回的类型是double,但表示的是int类型的数据,因此需要强制类型转换为int。
例:Math.floor(1.5) = 1; Math.floor(-1.5) = -2;
(2)long round(double a):首先要注意的是这里返回值的类型是long,但如果Math(11.5f),那么它的返回值类型就是int。简单来说就是我们平常使用的四舍五入。内部实现方法为:Math,floor(x + 0.5),即将原来的数+0.5,再取不超过这个数的最大整数。
(3)double ceil(double a):表示向上取整,即大于a的最小整数。
(4)double random():产生随机数,且随机数的范围是:[0,1);如果需要生成指定范围内的随机数可以通过以下代码实现:
public static double random(double min,double max){
double r = Math.random();
return min +(max - min) * r;
}
Integer的==比较
Integer是int的包装类,int的初始值是0,integer的初始值为null。
- public static void main(String[] args) {
- Integer a1=1;
- Integer a2=1;
- Integer b1=200;
- Integer b2=200;
- Integer c1=new Integer(1);
- Integer c2=new Integer(1);
- Integer d1=new Integer(200);
- Integer d2=new Integer(200);
- System.out.println("a1==a2?"+(a1==a2));
- System.out.println("b1==b2?"+(b1==b2));
- System.out.println("c1==c2?"+(c1==c2));
- System.out.println("d1==d2?"+(d1==d2));
}
运行结果是:
- a1==a2?true
- b1==b2?false
- c1==c2?false
- d1==d2?false
先比较a
和b
两组,Integer
初始化时,缓存Integer
对象数据,这些Integer
对象对应的int
值为byte
范围,即[-128,127]
。当直接给Integer
赋int
值时,如果值得范围为[-128,127]
,Integer
直接从缓存中取Integer
对象,因此,当直接赋的int
值处于[-128,127]
时,Integer
对象为相同的对象。而通过new
方式取的Integer
对象,是直接从堆里面分配的对象,因此,不管具体的int
值为多少,==判断的结果都是false
。
二、String相关内容的学习
equals和==的区别
“==”:对于基本类型比较的是它们的值,而对于复合类型则比较的是内存地址。
“equals”:该方法属于Object类,而所有的类都继承Object类,因此每个类中都有这个方法。
在Object类中equals方法的实现代码如下:
boolean equals(Object o){
return this==o;
}
这说明,如果一个类没有自己定义的equals方法,它默认的equals方法就是使用==操作运算符,也就是判断两个变量所指向的对象是否为同一对象。但在库中的一些类会覆盖重写equals方法,如:String、Integer、Date这些类中equals中有自身的实现,而不再是比较类在堆内存的地址。比如我们在String中的equals方法,首先判断==,如果地址相同,则一定返回true;如果地址不相同,在进行比较字符串内容是否相同。
String、StringBuffer以及StringBuilder的区别
String:字符串常量。
StringBuffer:字符串变量(线程安全)。
StringBuilder:字符串变量(非线程安全)。
简单地说,String类型和StringBuffer类型的主要性能区别在于String是不可变对象,因此在每次对String类型进行改变的时候都等同于生成一个新的String对象,然后将指针指向新的对象;而StringBuffer类则结果不同了,每次结果都会对StringBuffer本身进行操作,而不是生成新的对象,再改变对象引用。在字符串经常改变的情况下推荐使用StringBuffer。
大部分情况下:StringBuffer > String
StringBuffer的主要操作是append和insert方法,可以重载这些方法,来接受任意类型的数据。每个方法都能有效地将给定的数据转换成字符串,然后将字符串的字符追加或插入到字符串缓冲区中。append方法始终将这些字符添加到缓冲区的末端,insert方法在指定位置添加字符。
例:如果 z 引用一个当前内容是“start”的字符串缓冲区对象,则此方法调用 z.append("le") 会使字符串缓冲区包含“startle”,而 z.insert(4, "le") 将更改字符串缓冲区,使之包含“starlet”。
大部分情况下:StringBuilder > StringBuffer
该类设计用作StringBuffer的简易替换,它比StringBuffer要快。
(1)字符串是否可变
String:使用字符数组保存字符串,private final char value[];
关键字final决定了String对象不可变。
StringBuffer、StringBuilder继承自 AbstractStringBuilder类,没有final,因此决定了它可变。
(2)线程安全
String对象不可变,可以理解为常量,线程安全;
StringBuffer对方法加了同步锁,线程安全;、
StringBuilder没有加锁,是非线程安全的。
String常用函数
split方法:将一个字符串分割为子字符串,然后将结果作为字符串数组返回。
split函数原型是:public String[ ] split(String regex,int limit):split函数是用于使用特定的切割符(regex)来分隔字符串成一个字符串数组,函数返回是一个数组。在其中每个出现regex的位置都要进行分解。
(1)regex是可选项。字符串或正则表达式对象,它标识了分隔字符串时使用的是一个还是多个字符。如果忽略该选项,返回包含整个字符串的单一元素数组。
(2)limit也是可选项。该值用来限制返回数组中的元素个数。
(3)要注意转义字符:“.”和“|”都是转义字符,必须得加"\\"。同理:*和+也是如此的。
如果用“.”作为分隔的话,必须是如下写法:String.split("\\."),这样才能正确的分隔开,不能用String.split(".");
如果用“|”作为分隔的话,必须是如下写法:String.split("\\|"),这样才能正确的分隔开,不能用String.split("|");
(4)如果在一个字符串中有多个分隔符,可以用“|”作为连字符,比如:“acountId=? and act_id =? or extra=?”,把三个都分隔出来,可以用String.split("and|or");
当字符串只包含分隔符时,返回数组没有元素;
当字符串不包含分隔符时,返回数组只包含一个元素(该字符串本身);
字符串最尾部出现的分隔符可以看成不存在,不影响字符串的分隔;
字符串最前端出现的分隔符将分隔出一个空字符串以及剩下的部分的正常分隔;
replace(两个重载函数)、replaceAll、replaceFirst
- replace:原型为String replace(char oldChar,char newChar),即将所有的oldChar字符替换成newChar字符。
- replace:原型为String replace(CharSequence target,CharSequence replacement),将所有的target字符串替换成replacement字符串。
- replaceAll:原型为String replaceAll(String regex,String replacement),参数regex为一个正则表达式,replacement为替换的新字符串,即将原字符串中所有满足正则表达式regex的部分替换为replacement。
- replaceFirst:原型为String replaceFirst(String regex,String replacement),替换第一个满足正则表达式regex的部分。
正则表达式
public static void main(String[] args) {
String dataStr = "--->我是干扰字符<---M12v,L23f,d34";
Pattern pattern = Pattern.compile("[a-zA-Z]");
Matcher matcher = pattern.matcher(dataStr);
// 遍历匹配正则表达式的字符串
while (matcher.find()) {
// s为匹配的字符串
String s = matcher.group();
System.out.println(s);
}
}
运行结果为:
M v L f d
三、ArrayList、LinkedList、Vector区别
继承结构:
Collection
├------List
├----LinkedList
├----ArrayList
├----Vector
(1)ArrayList
定义:public class ArrayList<E> extends AbstractList<E> implements List<E>,RandomAccess,Cloneble,Serializable
特性:
- 可变大小的数组;
- 非线程安全;
- 当更多的元素加入ArrayList时,其大小会动态的增长。每次增长的空间是其size的50%,初始容量是10;
- 允许null元素。
(2)LinkedList
定义:public class LinkedList<E> extends AbstractList<E> implements List<E>,Deque<E>,Cloneble,Serializable
特性:
- 是一个双链表;
- 非线程安全;
- 在添加和删除元素时具有比ArrayList有更好的性能;
- LinkedList还实现了Queue接口(非直接实现,是通过实现Queue的子接口Deque间接实现Queue),该接口比List提供了更多方法。包括从尾部添加元素:offer(E)、返回第一个元素但不出队:peek()、返回第一个元素并出队:poll()等;
- 允许null元素。
(3)Vector
定义:public class Vector<E> extends AbstractList<E> implements List<E>,RandomAccess,Cloneble,Serializable
特性:
- Vector与ArrayList相似,但属于强同步类;
- 比ArrayList多了线程安全;
- 默认每次动态增加的空间是当前大小的2倍,如果在构造函数Vector(int initialCapacity,int capacityIncrement)中指定了capacityIncrement,每次动态增加的大小为capacityIncrement;
- 初始容量是10;
- 允许null元素。
(4)对比表
长度可变 | 线程安全 | 扩容倍数 | |
ArrayList | 是 | 否 | 0.5(初始10) |
LinkedList | 是 | 否 | —— |
Vector | 是 | 是 | 2(初始10) |
四、Map相关子类区别
继承关系
//HashMap
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable{}
//Hashtable
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {}
//ConcurrentHashMap
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable {}
//TreeMap
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable{}
//LinkedHashMap
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>{}
HashMap内部实现
HashMap本质是数组加链表。根据key值取得hash值,然后计算出数组下标,如果多个key对应到同一个下标,就用链表串起来。新插入的在前面。不能保证该映射顺序恒久不变,里面存放的是Map.Entry类,该类的本质是个键值对。
- HashMap数据结构:根据key的hashCode来计算hash值,只要hashCode相同,计算出来的hash值就一样。出现hash冲突的情况就采用链表的方式,将相同的hash值的对象用链表连接。
- HashMap存取:put新元素时,首先根据key的hashCode来重新计算hash值(二次hash),根据这个新的hash值得到这个元素在数组的位置(下标),如果数组已经存放其他元素,那么该位置的元素以链表的形式存放,新加入的放在链头位置,最先加入的放在链尾位置。根Key的hashCode二次hash的算法函数hash(int h),此方法加入了高位运算,防止地位不变而高位变化时造成的hash冲突。函数具体实现如下(
>>>
表示右移1位并忽略符号位,空位以0
补齐。而>>
表示右移不忽略符号位,即相当于除以2
):
static int hash(int h){
h ^= (h>>>20)^(h>>>12);
return h^(h>>>7)^(h>>>4);
}
此时得到了二次hash,二次hash的主要目的就是将高位引入计算,使得计算出来的位置值与高位有关。将二次hash值对数组长度取模运算使得元素分布比较均匀。但是模运算的消耗比较大。在HashMap中这样完成:indexFor(int h,int length)方法计算该对象应该保存在table数组的那个索引处:
static int indexFor(int h,int length){
return h & (length-1);
}
通过 h & (length-1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,即HsahMap速度上的优化。
HashMap扩容(resize,rehash):每次数组扩容为原来的两倍。扩容会带来性能上的问题,就是每次扩容需要重新计算某个元素的位置。
Fail-Fast(快速失败)机制:HashMap不是线程安全的,因此在使用迭代器过程中,其他线程修改了Map,那么将抛出ConcurrentModificationException异常,这就是fail-fast策略。其实现原理为:通过modCount域【顾名思义,就是修改次数,对HashMap内容的修改都将增加这个值,在迭代器初始化会将这个值赋值给迭代器的expectedModCount,迭代过程中,判断modCount和expectedModCount是否相等,如果不相等则表示已经有其他的线程修改了Map】。
ConcurrentHashMap
在HashMap基础上,ConcurrentHashMap将数据分为多个segment,默认16个,然后每次操作对一个segment加锁,避免多线程锁的几率,提高并发效率。
HashTable和HashMap的区别
HashMap父类为AbstractMap,方法不同步,K,V可为null,添加新的kv,若k相同,则将新的v覆盖。
HashTable父类为Dictionary,方法同步,k,v不可为null,添加新的kv,若k相同,则将新的v覆盖。
TreeMap、HashMap、LinkedHashMap的区别
- 我们在开发的过程中使用HashMap比较多,在Map中插入、删除和定位元素,HashMap 是最好的选择。
- 但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。
- 如果需要输出的顺序和输入的相同,那么用LinkedHashMap 可以实现,它还可以按读取顺序来排列.
- TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值升值排序,也可以指定排序的比较器(通过构造器传入Comparator对象),当用iterator遍历TreeMap时,得到的记录是排过序的。
- LinkedHashMap,是HashMap子类,保存了记录的 插入顺序,在用iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,如果需要输出的顺序和输入的相同,那么LinkedHashMap可以实现。LRU算法里面使用到LinkedHashMap,其优于LinkedList的特点是:取值速度快;免去了遍历搜索过程。
五、synchronized、lock、reentrantLock的区别
synchronized
当用来修饰一个方法或者一个代码块时,能够保证在同一时刻最多只有一个线程 执行该段代码,它是在软件层面依赖JVM实现同步。
synchronized方法或语句的使用提供了对于每个对象相关的隐式监视器锁的访问,但是却强制所有锁获取或释放均要出现在一个块结构中:
当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的词法范围内释放所有锁。
synchronized使用总结如下:
- 对象锁钥匙只能有一把才能互斥,才能保证共享变量的唯一性。
- 在静态方法上的锁,和实例方法上的锁,默认不是同样的,如果同步需要制定两把锁一样。
- 关于同一个类方法上的锁,来自于调用该方法的对象,如果调用该方法的对象是相同的,那么锁必然相同。比如:new A().x()和new A().x(),对象不同,锁不同,如果A是单例的,就能互斥。
- 静态方法加锁,能和所有其他静态方法加锁的进行互斥。
- 静态方法加锁,和xx.class锁效果一样,直接属于类的。
synchronized的缺陷:
若将一个大的方法声明为synchronized将会大大的影响效率,典型地,若将线程类的方法run()声明为synchronized,由于在线程的整个生命周期内它一直在运行,因此将导致它对本类任何synchronized方法的调用永远都不会成功。
解决办法:
//通过synchronized关键字来声明synchronized块
synchronized(syncObject){
//访问或修改被锁保护的共享状态
}
其中的代码必须获得对象syncObject(类实例或类)的锁方能执行。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。
当两个并发线程访问同一个对象中的synchronized(this)同步代码块时,在此时间内只能有一个线程得到执行。 另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块。其他线程对对象中所有其他synchronized(this)同步代码块的访问将被阻塞。
如果线程进入由线程已经拥有的监控器保护的synchronized块,就允许线程继续进行,当线程退出第二个(或者后续)synchronized块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个synchronized时,才释放锁。
在修饰代码块的时候需要一个reference对象作为锁的对象。
在修饰方法的时候默认是当前对象作为锁的对象。
在修饰类时默认是当前类的Class对象作为锁的对象。
Lock
Lock
接口实现提供了比使用synchronized
方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的Condition
对象。在硬件层面依赖特殊的CPU
指令实现同步更加灵活。
什么是condition
Condition
接口将Object
监视器方法(wait
、notify
和 notifyAll
)分解成截然不同的对象,以便通过将这些对象与任意 Lock
实现组合使用,为每个对象提供多个等待set
(wait-set
)。其中,Lock
替代了synchronized
方法和语句的使用,Condition
替代了 Object
监视器方法的使用。
虽然synchronized
方法和语句的范围机制使得使用监视器锁编程方便了很多,而且还帮助避免了很多涉及到锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。随着灵活性的增加,不使用块结构锁就失去了使用synchronized
方法和语句时会出现的锁自动释放功能。在大多数情况下,应该使用以下语句:
Lock l = ...;//lock接口的实现类对象
l.lock();
try{
//access the resource protected by this lock
}finally{
l.unlock();
}
在java.util.concurrent.locks
包中有很多Lock
的实现类,常用的有ReentrantLock
、ReadWriteLock
(实现类ReentrantReadWriteLock
).它们是具体实现类,不是Java
语言关键字。
ReentrantLock
一个可重入的互斥锁Lock
,它具有与使用synchronized
方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
典型代码:
class X{
private final ReentrantLock lock = new ReentrantLock();
private void m(){
lock.lock();
//block until condition holds
try{
//...method body
}fianlly{
lock.unlock();
}
}
}
重入性:指的是同一个线程多次试图获取它所占有的锁,请求会成功。当释放锁的时候,直到重入次数清零,锁才释放完毕。
ReentrantLock
的lock
机制有2种,忽略中断锁和响应中断锁,这给我们带来了很大的灵活性。比如:如果A
、B
2个线程去竞争锁,A
线程得到了锁,B
线程等待,但是A
线程这个时候实在有太多事情要处理,就是一直不返回,B
线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候ReentrantLock
就提供了2
种机制,第一,B
线程中断自己(或者别的线程中断它),但是ReentrantLock
不去响应,继续让B
线程等待,你再怎么中断,我全当耳边风(synchronized
原语就是如此);第二,B
线程中断自己(或者别的线程中断它),ReentrantLock
处理了这个中断,并且不再等待这个锁的到来,完全放弃。
ReentrantLock
相对于synchronized
多了三个高级功能:
1.等待可中断
在持有锁的线程长时间不释放锁的时候,等待的线程可以选择放弃等待.
- tryLock(long timeout, TimeUnit unit)
2.公平锁
按照申请锁的顺序来一次获得锁称为公平锁.synchronized的是非公平锁,ReentrantLock可以通过构造函数实现公平锁.
- new RenentrantLock(boolean fair)
公平锁和非公平锁。这2种机制的意思从字面上也能了解个大概:即对于多线程来说,公平锁会依赖线程进来的顺序,后进来的线程后获得锁。而非公平锁的意思就是后进来的锁也可以和前边等待锁的线程同时竞争锁资源。对于效率来讲,当然是非公平锁效率更高,因为公平锁还要判断是不是线程队列的第一个才会让线程获得锁。
3.绑定多个Condition
通过多次newCondition
可以获得多个Condition
对象,可以简单的实现比较复杂的线程同步的功能.通过await()
,signal()
synchronized和lock的用法与区别
synchronized
是托管给JVM
执行的,而Lock
是Java
写的控制锁的代码。synchronized
原始采用的是CPU
悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU
转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU
频繁的上下文切换导致效率很低。Lock
用的是乐观锁方式。每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。ReentrantLock
必须在finally中释放锁,否则后果很严重,编码角度来说使用synchronized
更加简单,不容易遗漏或者出错。ReentrantLock
提供了可轮询的锁请求,他可以尝试的去取得锁,如果取得成功则继续处理,取得不成功,可以等下次运行的时候处理,所以不容易产生死锁,而synchronized
则一旦进入锁请求要么成功,要么一直阻塞,所以更容易产生死锁。synchronized
的话,锁的范围是整个方法或synchronized
块部分;而Lock
因为是方法调用,可以跨方法,灵活性更大
一般情况下都是用synchronized
原语实现同步,除非下列情况使用ReentrantLock
:
- 某个线程在等待一个锁的控制权的这段时间需要中断
- 需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程
- 具有公平锁功能,每个到来的线程都将排队等候
六、线程池的使用
线程池基础
配置线程池一般如下语句:
public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor( CORE_POOL_SIZE,MAXIMUM_POOL_SIZE,KEEP_ALIVE,TimeUnit.SECONDS,
sPoolWorkQueue,sThreadFactory
);
当一个任务加到线程池时:
- 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
- 如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。
- 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
- 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
- 当线程池中的线程数量大于 corePoolSize时,如果某线程(非核心线程)空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。
处理任务的优先级为:
- 核心线程corePoolSize
- 任务队列workQueue
- 最大线程maximumPoolSize
如果三者都满了,使用handler
处理被拒绝的任务(一般为抛出java.util.concurrent.RejectedExecutionException
异常)。
线程池类型
线程池主要有以下四种类型:
FixedThreadPool:线程数量固定的线程池,线程处于空闲状态时不会被回收,除非线程被关闭。当所有线程都处于活动状态时,新的任务都会处于等待状态,直到有线程空闲出来。
CachedThreadPool:线程数量不固定,只有非核心线程,可以放任意多个线程(
Integer.MAX_VALUE
),线程池里所有线程处于活动状态时,创建新的线程处理新来的任务。否则利用闲置的线程处理新任务。线程池里空闲线程有超时机制,时长为60
秒。ScheduledThreadPool:核心线程数量是固定的,非核心线程是没有限制。当非核心线程闲置时会被立即回收。
SingleThreadExector:内部只有一个核心线程,确保所有任务在同一个线程中按顺序执行。
线程池的使用方法
Runnable task=new Runnable(){
Public void run(){
//TODO .......
}
};
//FixedThreadPool使用
ExecutorService fixedThreadPool=Executors.newFixedThreadPool(4);
fixedThreadPool.execute(task);
//CachedThreadPool的使用
ExecutorService cachedThreadPool=Executors.newCachedThreadPool();
cachedThreadPool.execute(task);
//ScheduledThreadPool的使用
ExecutorService scheduledThreadPool=Executors.newScheduledThreadPool(4);
//2000ms后执行task
scheduledThreadPool.schedule(task,2000,TimeUnit.MILLISECONDS);
//延迟10ms后,每隔1000ms执行一次task
scheduledTheadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);
//SingleThreadExector的使用
ExecutorService sigleThreadPool=Executors.newSingleThreadExecutor();
fixedThreadPool.execute(task);
线程池的优点
- 重用线程池中的线程,避免因为线程的创建和性能所带来的性能开销;
- 能够有效控制线程池的最大并发数,避免大量线程之间因互相抢占系统资源而导致阻塞;
- 能够对线程进行简单的管理,并提供定时执行以及指定间隔循环执行等功能。
七、interface与abstract的区别
interface和abstract类的区别如下:
继承方面:
abstract class在java中表示的是一种继承关系,一个类只能使用一次继承关系。但是一个类可以实现多个interface。
成员变量方面:
在abstract class中可以有自己的数据成员,也可以有非abstract的方法,而在interface中,只能够有静态的不能被修改的数据成员(也就是static final 的,不过在interface中一般不定义数据成员),所有的方法都是public abstract的。
抽象方法方面:
实现抽象类和接口的类必须实现其中所有的抽象方法,抽象类中可以有非抽象方法,而接口中所有方法为抽象方法。
访问权限方面:
抽象类的变量默认是friendly型,其值可以在子类中重新定义,也可以重新赋值,接口中定义的变量默认是public static final型,且必须给其赋初值,所以实现类中不能重新定义,也不能改变其值。
设计理念方面:
abstract class和interface所反映出的设计理念不同。其中abstract class表示的是“is-a”关系,interface表示的是“like-a”关系。