1. [Map、Set、List、Queue、Stack的特点与用法](#map-set-list-queue-stack的特点与用法)
2. [HashMap和HashTable的区别](#hashmap和hashtable的区别)
3. [](#)
4. [HashMap、LinkedHashMap和TreeMap的区别](#hashmap-linkedhashmap和treemap的区别)
5. [Collections类和Arrays类](#collections类和arrays类)
6. [子类和父类的构造方法、super()、super、this()、this](#子类和父类的构造方法-super-super-this-this)
1. [子类初始化时需要调用父类的构造方法吗?](#子类初始化时需要调用父类的构造方法吗)
2. [父类必须声明无参构造方法吗?](#父类必须声明无参构造方法吗)
3. [super()、this()的调用必须在构造方法的第一行吗?](#super-this的调用必须在构造方法的第一行吗)
4. [super、this关键字](#super-this关键字)
7. [权限修饰符和abstract、final修饰符](#权限修饰符和abstract-final修饰符)
8. [Java面向对象的三大特征](#java面向对象的三大特征)
1. [封装](#封装)
2. [继承](#继承)
3. [多态](#多态)
9. [重写(Override)与重载(Overload)的区别](#重写override与重载overload的区别)
10. [面向对象五大原则:](#面向对象五大原则)
1. [单一职责原则(Single-Responsibility Principle)](#单一职责原则single-responsibility-principle)
2. [开放封闭原则(Open-Closed Principe)](#开放封闭原则open-closed-principe)
3. [里氏替换原则(Lishov-Substitution Principle)](#里氏替换原则lishov-substitution-principle)
4. [依赖倒置原则(Dependency-InVersion Principle)](#依赖倒置原则dependency-inversion-principle)
5. [接口隔离原则(Interface-Segregation Principle)](#接口隔离原则interface-segregation-principle)
11. [抽象类和接口](#抽象类和接口)
1. [定义](#定义)
2. [对比和区别](#对比和区别)
3. [适用场景](#适用场景)
4. [Java8中的默认方法和静态方法](#java8中的默认方法和静态方法)
Map、Set、List、Queue、Stack的特点与用法
Java容器分类图(虚线框表示抽象类):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0EoXvEOM-1659947540430)(https://img-blog.csdn.net/20180228071031348?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdmFwaW5nemk=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)]
Map:
Map即映射表,里面保存的是一组成对的“键值对”对象,一个映射不能包含重复的键,但可以包含重复的值,每个键最多只能映射到一个值,我们可以通过“键”找到该键对应的“值”。
常见的Map的实现类:
- HashMap:哈希映射表,最常用的Map,使用了hashCode(散列码)优化的Map结构,不保证遍历顺序(例:先存入a后存入b,但遍历时可能先出现b再出现a)
- LinkedHashMap:HashMap的子类,但内部维护了一个双向链表,保证遍历顺序是插入顺序或基于LRU(最近最少使用)算法
- HashTable:与HashMap大致上类似,但是是线程安全的
- TreeMap:基于红黑树实现的HashMap,查看“键”或“键值对”时,它们会被排序,另外可以使用subMap方法返回子树
- WeakHashMap:使用弱引用实现的HashMap,当其中的某些键值对不再被使用时会被自动GC掉
- IdentityHashMap:使用==代替equals对“键”进行比较的HashMap
Set:
Set即集合,里面保存的是不可重复的元素。通过equals()方法来确保对象的唯一性。
常见的Set的实现类:
- HashSet:为快速查找而设计的Set,存入的元素必须定义hashCode
- LInkedHashSet:具有HashSet的查询速度,且内部有链表维护元素顺序
- TreeSet:保持排序的Set,底层为树结构
List:
List即列表,与数组类似,按照元素插入的顺序保存元素。
常用的LIst实现类:
- ArrayList:底层为可变长数组,用于查找较多的列表
- LinkedList:底层为双向链表,用于插入较多的列表
- Vector:与ArrayList类似,不同的是Vector是线程安全的,相对效率较低
Queue:
Queue即队列,是一个FIFO(先进先出)的容器。
常用的Queue的实现类:
- LinkedList:即普通的队列
- PriorityQueue:一个由优先级堆实现的对类,队列中的元素是有序的
Stack:
Stack即栈,是一个LIFO(后进先出)的容器。
Stack没有其他的实现类,直接使用Stack。
HashMap和HashTable的区别
HashMap和HashTable都实现了Map接口,功能也类似,两者的主要区别如下:
- HashMap继承自AbstractMap类,而HashTable继承自Dictionary类
- HashMap不是线程安全的,HashTable是线程安全的,因此HashTable的效率相对较低
- HashMap允许key和value为null,而HashTable两者都不允许为null
虽然 HashTable 是线程安全的,但同样不适用于并发环境编程,因为 synchronize 会导致性能下降,限制了并发量。
扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免多次扩容
HashMap可以通过以下方式实现线程安全
Map m = Collections.synchronizeMap(hashMap);
或者使用util.concurrent包下的ConcurrentHashMap。
HashMap和ConcurrentHashMap的区别
HashMap、LinkedHashMap和TreeMap的区别
HashMap就不用多说了,一般情况下访问速度比LinkedHashMap快。
LinkedHashMap:
LinkedHashMap是HashMap的子类,其类的声明如下:
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>{
transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
}
其与HashMap最大的不同在于内部维护了一个用于遍历的双向链表,遍历的时候能够保证顺序为元素插入的顺序。
在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。
TreeMap:
TreeMap实现底层为红黑树,因此插入TreeMap的元素遍历时也是有序的(由Comparator决定排序的方式)。
TreeMap在对存放的元素进行排序时有两种方式,一种是通过自身持有的Comparator进行排序,另一种是通过实现了Comparable接口的key进行排序,优先使用第一种方式,当持有的Comparator(默认为null)为null时则采用第二种方式。
当通过Comparator比较了现有元素的key与当前存放元素的key的结果为0时,将认为当前存放的元素key在原有Map中已经存在,然后改变原有的key对应的value为新value,然后就直接返回了旧value(JDK1.8后的HashMap处理冲突的方式也是这样的,同样使用的红黑树)。当持有的Comparator为空时将通过实现了Comparable接口的key的compareTo方法来决定元素存放的位置,有一点与使用Comparator类似的地方是当原有key作为Comparable与新存入的key进行比较的结果为0时将认为新存入的key在原Map中已经存在,将直接改变对应的原key的value,而不再新存入key-value对。实际上其判断元素是否存在的containsKey方法的主要实现逻辑也是类似的
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
/**
* The comparator used to maintain order in this tree map, or
* null if it uses the natural ordering of its keys.
*
* @serial
*/
private final Comparator<? super K> comparator;
private transient Entry<K,V> root;
}
Collections类和Arrays类
在java.util 包中,除了Collection类以外,还有两个相关的工具类,分别是Collections和Arrays类。两个类的构造方法均为private方法。
两个类包含了大量的静态方法用于处理我们的存储结构(排序,二分查找,填充等操作)。其中,Collections用于处理Collection容器类的存储结构,而Arrays则用于处理基本类型组成的数组。
子类和父类的构造方法、super()、super、this()、this
子类初始化时需要调用父类的构造方法吗?
因为子类继承了父类,那么就默认的含有父类的公共成员方法和公共成员变量,这些方法和变量在子类中不再声明。为了使子类调用父类的方法或变量时能正确执行,在初始化子类时,就必须初始化父类。
-
如果父类的构造方法是带参数的,而且没有无参数的构造方法,那么在子类的构造方法中必须显示地调用父类的构造方法。
-
如果父类的构造方法是无参数的,那么在子类中写不写都可以,不写的话也会隐式调用父类的构造方法。
父类必须声明无参构造方法吗?
不一定。
在不声明构造方法的情况下,每个类会默认生成一个无参构造方法。但是在声明含参构造方法时,除非主动声明无参构造方法,否则不会自动生成。
为了保证子类中可以正常地声明构造方法,一般要求父类也声明无参构造方法。或者在子类的无参构造方法中,使用super(有参方法)的方法避免隐式调用父类的无参构造方法。
super()、this()的调用必须在构造方法的第一行吗?
是的。为什么一定要在第一行?
-
super()在第一行的原因就是: 子类有可能访问了父类对象, 比如在构造函数中使用父类对象的成员函数和变量, 在成员初始化使用了父类, 在代码块中使用了父类等, 所以为保证在子类可以访问父类对象之前要完成对父类对象的初始化
-
this()在第一行的原因就是: 为保证父类对象初始化的唯一性. 我们假设一种情况, 类B是类A的子类, 如果this()可以在构造函数的任意行使用, 那么会出现什么情况呢? 首先程序运行到构造函数B()的第一行, 发现没有调用this()和super(), 就自动在第一行补齐了super() , 完成了对父类对象的初始化, 然后返回子类的构造函数继续执行, 当运行到构造函数B()的"this(2) ;"时, 调用B类对象的B(int) 构造函数, 在B(int)中, 还会对父类对象再次初始化! 这就造成了对资源的浪费, 当然也有可能造成某些意想不到的结果, 不管怎样, 总之是不合理的, 所以this() 不能出现在除第一行以外的其他行!
class B extends A {
B() {
//这里,编译器将自动加上 super();
System.out.println("You call subclass constructor!");
}
B(String n) {
super();
this();//实际就是调用了B(){...},而在B(){...}中编译器自动加上了
//super();这样就相当于两次调用了super();也就是说对父类进//行了两次初始化。而在实例化一个对象时,一个构造方法只能调用一次,这说明this和super不能同时存在一个构造方法中。同时因为系统没有在第一行发现this()或super()调用,就会自动加上super(),如果没有将this()和super()放在第一行就会产生矛盾。 //因为总有一个super()在第二句上。所以该程序不能通过编译!!!
}
}
super、this关键字
super关键字表示超(父)类的意思。this变量代表对象本身。
-
可以使用super访问父类被子类隐藏的变量或覆盖的方法(但不能访问本来就没有访问权限的,比如私有的成员变量或方法)。当前类如果是从超类继承而来的,当调用super.XX()就是调用基类版本的XX()方法。
-
当类中有两个同名变量,一个属于类(类的成员变量),而另一个属于某个特定的方法(方法中的局部变量),使用this区分成员变量和局部变量。
权限修饰符和abstract、final修饰符
public | protect | 无修饰(default) | private | abstract | final | static | |
---|---|---|---|---|---|---|---|
类继承 | 可继承 | (不能修饰类) | 只有同一包中的类可继承 | (不能修饰类) | 一般可继承(具有访问权限) | 不能派生子类 | (不能修饰类) |
方法重载 | 可重载 | 可重载 | 可重载 | 不可重载 | 可重载 | 不可重载 | 一般可重载(修饰main()方法时不可重载) |
成员变量的访问权限 | 父类属性被隐藏(可使用super关键字获取) | 父类属性被隐藏 | 父类属性被隐藏 | 子类不能直接访问父类的私有变量 | (不能修饰成员变量) | 必须赋初值 | 每个实例共享这个类变量 |
Java面向对象的三大特征
Java中的面向对象的三大基本特征分别是:封装、继承、多态:
封装
所谓封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或对象操作,对不可信的进行信息隐藏。
封装是面向对象的特征之一,是对象和类概念的主要特性。简单的说,一个类就是一个封装了数据以及操作这些数据的方法的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误地使用了对象的私有部分。
继承
继承是指这样一种能力:它可以使现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊,从抽象到具体的过程。要实现继承,可以通过“继承”(Inheritance)和“组合”(Compostion)来实现。继承概念的实现方式有两类:实现继承和接口继承。实现继承是指直接使用基类的属性和方法而无需额外编码的能力;接口继承是指仅使用属性和方法的名称,但子类必须具备实现方法的能力。
多态
所谓多态就是指一个类实例的相同方法在不同情形有不同表现形式。
实现多态的三个必要条件:继承、重写、向上转型。可以通过继承实现,也可以通过接口实现。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们可以通过相同的方式进行调用。最常见的多态就是将子类传入父类参数中,运行时调用父类方法时,会通过传入的子类决定具体的内部结构或行为。好处是程序的可扩展性及可维护性增强。
重写(Override)与重载(Overload)的区别
- 重载主要发生在同一个类的多个同名方法之间;重写发生在子类和父类的同名方法之间
- 重载是在一个类中定义多个形参列表不同的同名方法;重写是在子类中定义与父类形参列表相同的同名方法,需要加上@Override注解。两者都需要方法名相同,并无太大的相似之处。
面向对象五大原则:
单一职责原则(Single-Responsibility Principle)
一个类应该仅有一个引起它变化的原因。
职员类例子: 比如在职员类里,将工程师、销售人员、销售经理这些情况都放在职员类里考虑,其结果将会非常混乱,在这个假设下,职员类里的每个方法都要ifelse判断是哪种情况,从类结构上来说将会十分臃肿,并且上述三种的职员类型,不论哪一种发生需求变化,都会改变职员类!这个是大家所不愿意看到的!
开放封闭原则(Open-Closed Principe)
对扩展是开放的,对更改是封闭的。
变化来临时,如果不必改动软件实体裁的源代码,就能扩充它的行为,那么这个软件实体设计就是满足开放封闭原则的。如果说我们预测到某种变化,或者某种变化发生了,我们应当创建抽象类来隔离以后发生的同类变化。
里氏替换原则(Lishov-Substitution Principle)
子类可以替换父类并且出现在父类能够出现的任何地方,贯彻GOF倡导的面向接口变成。
在这个原则中父类应尽可能使用接口或者抽象类来实现!通过这个原则,我们客户端在使用父类接口时,通过其子类来实现并替换父类,这样做的好处就是,在根据新要求扩展父类的时候不影响当前客户端的使用。
依赖倒置原则(Dependency-InVersion Principle)
传统的结构化编程中,最上层的模块通常都要依赖下面的子模块来实现,也称为高层依赖低层。所以DIP原则就是要逆转这种依赖关系,让高层模块不要依赖低层模块,所以称之为依赖倒置原则。
接口隔离原则(Interface-Segregation Principle)
使用多个专门的接口比使用单个复杂的接口要好得多。
抽象类和接口
定义
抽象类:是用来捕捉子类的通用特性的。它不能被实例化,只能作为子类的超类。抽象类是用来创建继承层级里子类的模板。
接口:接口是抽象方法的集合。如果一个类实现了某个接口,那么它就继承了这个接口的抽象方法。这就像契约模式,如果实现了这个接口,那么就必须确保使用这些方法。接口只是一种形式,接口自身不能做任何事情。
对比和区别
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9pv0fekz-1659947540432)(https://img-blog.csdn.net/20180228071011822?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdmFwaW5nemk=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)]
注意事项
-
抽象类和接口都不能实例化,实例化的工作应该交由它的子类实现,它只需要有一个引用即可,但可以通过匿名内部类的方式使用(实际上是实例化一个它的子类)
-
abstract方法一定要它的非抽象子类去实现
-
abstract不能与final关键字一起使用,也不能和private、static、native并列修饰同一个方法
-
抽象类中可以包含具体的方法,也可以不包含抽象方法
-
子类的抽象方法不能与父类的抽象方法同名
-
只要包含一个抽象方法,该类就必须为抽象类
-
一个类最多只能继承一个抽象类,而一个类可以实现多个接口
-
接口中在JDK1.8后,可以定义非抽象(default修饰)方法和static方法
-
如果一个接口只定义了一个抽象方法,那么这个接口就成了函数式接口(这些方法可以在接口中直接运行)
-
外部接口中所有方法的访问权限都会自动声明为public abstract,并且不能定义为其他,类内部的接口则没有这些要求
-
接口中可以定义“成员变量”,或者说是不可变的常量,因为接口中的“成员变量”会自动变为public static final。
-
不能使用new操作符实例化一个接口,但可以声明一个接口变量,该变量必须引用一个实现该接口的类的对象。可以使用instanceof检查一个对象是否实现了某个特定的接口。
适用场景
- 如果你拥有一些方法并且想让它们中的一些有默认实现,那么使用抽象类吧。
- 如果你想实现多重继承,那么你必须使用接口。由于Java不支持多继承,子类不能够继承多个类,但可以实现多个接口。因此你就可以使用接口来解决它。
- 如果基本功能在不断改变,那么就需要使用抽象类。如果不断改变基本功能并且使用接口,那么就需要改变所有实现了该接口的类。
Java8中的默认方法和静态方法
Oracle已经开始尝试向接口中引入默认(default)方法和静态方法,以此来减少抽象类和接口之间的差异。现在,我们可以为接口提供默认实现的方法了并且不用强制子类来实现它。