哈喽大家好我又回来了,一月不见大家练习的如何,学习有没有偷懒那(─‿‿─)。哈哈,从今天开始大家继续一起努力吧,相信学完基础篇的内容大家都应该可以用Java来做一些简单的小东西了,但Java这门语言可以长居语言榜单第一名肯定不只有这些功能,所以这一篇开始我们要探讨一些Java的中级特性,让大家对Java有更深的理解。可能接下来的内容可能难度要提升一些,毕竟基础篇的内容大家在大学有C语言的基础接受起来相对容易些,后面的内容涉及的范围要更广。不过还是那句话,编成语言是个熟练工,反复的练习是你掌握它的最好方法。中篇我刚学完的时候也是一脸懵逼,但真正写过几次后感觉就不一样了,我相信大家肯定比我要聪明些,只要练几遍肯定很快掌握。闲话少说开始我们的正题吧!
容器基本概念
容器说白了就是一个可以把一些数据收集起来的一个对象,像之前我们提到的数组就是容器,但长度固定的特点让数组有很大的局限性,为了方便我们对数据进行存取,Java为我们提供了更加简单易用的Collection接口和Map接口。
首先我们来看一下容器接口的继承关系:
Collection接口定义了存取一组对象的方法,它的子接口包括Set(无序不可重复对象)、list(有序可重复对象)。Map接口定义了存储“键-值映射对”的方法。Queue接口是一种模拟队列的数据结构,新元素插入(offer)到队列的尾部,访问元素(poll)操作会返回队列头部的元素。通常,队列不允许随机访问队列中的元素。本次讲解我们以List和Set为主。
List容器
首先查看List接口的结构:
图中标A的代表抽象方法,标D的代表default方法,它是在java8中引入的关键字,也可称为Virtual extension methods——虚拟扩展方法。是指,在接口内部包含了一些默认的方法实现(也就是接口中可以包含方法体,这打破了Java之前版本对接口的语法限制),从而使得接口在进行扩展的时候,不会破坏与接口相关的实现类代码。
List的两个实现类ArrayList和LinkList都需要实现List中的所有抽象方法,方法的作用都是一样的,只是不同的List针对自己的数据结构有不同的实现方法,以ArrayList为例这些方法的用途可以参考这里。
容器的使用不是什么难事,但作为开发者有必要了解一下它的底层实现,这不管是在面试还是今后学习更深层的知识都很重要,所以这次我打算对每个要讲到容器都做一下简单的实现。
ArrayList
ArrayList算是Java当中最简单也是最常用的容器了,它除了没有长度限制,其他操作和数组基本一模一样。当然它的底层也是通过数组实现的。这里我们对其进行简单实现,由于篇幅有限这次只实现几个常用方法,其他方法大家有兴趣可以自己去实现。
在讲解之前先介绍一个方法,该方法会在代码中多次使用:
System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
数组复制方法,各参数的含义为:
Object src : 原数组
int srcPos : 从元数据的起始位置开始
Object dest : 目标数组
int destPos : 目标数组的开始起始位置
int length : 要copy的数组的长度
初始化
首先确定ArrayList的底层是通过数组来实现,所以需要一个数组成员变量来对数据进行存储,同时我们也需要建立容器长度标识和构造方法,以及对异常的处理方法和数组扩容方法,代码如下:
public class MyArrayList{
private Object[] elementData;//存储数据元素
private int size;//容器元素个数
//获取容器长度
public int getSize() {
return size;
}
//检查输入是否合法,非法输入长度抛出异常
private void rangeCheck(int index) {
if(index<0||index>=size) {
try {
throw new Exception();
} catch (Exception e) {
e.printStackTrace();
}
}
}
//数组扩容
private void ensureCapacity() {
//如果数组满了就直接扩容
if (size==elementData.length) {
//扩大2倍多
Object[] newArray = new Object[size*2+1];
//旧数组复制到新数组中
System.arraycopy(elementData, 0, newArray, 0, elementData.length);
elementData = newArray;
}
}
//无参构造,默认初始化数组长度为10
public MyArrayList(){
this(10);
}
//有参构造
public MyArrayList(int initialCapacity){
rangeCheck(initialCapacity);
elementData = new Object[initialCapacity];
}
//获取指定元素
public Object get(int index) {
rangeCheck(index);
return elementData[index];
}
//判断是否为空
public boolean isEmpty() {
return size == 0;
}
//设置指定位置元素
public Object set(int index, Object obj) {
rangeCheck(index);
Object oldValue = elementData[index];
elementData[index] = obj;
return oldValue;
}
......
}
add方法
对于add方法我们实现两个,一个是在容器尾部添加的方法,另一个是在指定位置添加的方法
public void add(Object obj) {
//添加之前先进行扩容
ensureCapacity();
elementData[size++] = obj;
}
public void add(int index, Object obj) {
//检查输入是否合法,非法输入抛出异常
rangeCheck(index);
//数组扩容
ensureCapacity();
//注意由于是数组在指定位置添加之前数组元素要从这里开始后移一位。
System.arraycopy(elementData, index, elementData, index+1, size-index);
elementData[index] = obj;
size++;
}
remove方法
对remove方法我们也实现在尾部删除和指定索引删除两个:
public Object remove(int index) {
rangeCheck(index);
Object result = elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0) {
//与添加类似,删除元素后该位元素之后的元素要前移一位
System.arraycopy(elementData, index+1, elementData, index, numMoved);
}
elementData[--size] = null;
return result;
}
//删除指定元素
public void remove(Object obj) {
for (int i = 0; i < size; i++) {
//注意使用equals()方法,使得字符串等包装类可以通过值来判定
if (get(i).equals(obj)) {
remove(i);
}
}
}
以上就是ArrayList的简单实现,上面实现的方法也是ArrayList之中最常用的方法,大家可以在理解的基础上加以记忆,考虑到大家还没掌握太深所以用最简单的方式做了实现,当然源码中考虑的问题要更多所以要相对更复杂一些,但基本思路和我们的代码相同。
上述代码也很明显的反应了ArrayList的优势和弊端——数组的底层实现导致了查询容易,修改复杂。与之相对的就出现了LinkList。
LinkList
LinkList中的方法和ArrayList一模一样,只是底层通过链表来实现,所以相对ArrayList修改要容易,但查询会变难。
链表
在讲LinkList之前有必要先讲一下链表的基础知识,百度上链表的概念是这样的:
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
听起来很抽象,我们可以这样理解,我们在存储数据时会在内存中申请一块地址,现在我们可以把这块地址分成两份,一部分用来存储我们的数据,另一部分用来存储一个内存块地址,这个内存块地址中的数据同样分成两份,一部分用来存储我们的数据,另一部分用来存储一个内存块地址…这样,我们访问每一块内存地址时既可以访问到它存储的数据,又可以通过地址定位到另一块地址上,形成了一个链状结果,这就是所谓的链表。
图上图就是一个链表结构,头指针表示我们的引用变量,data、a1、a2代表链表中存储的数据,next代表下一个链表块的地址。当走到最后一个节点时,它的next处为null(也就是空)。
链表无论查询还是修改都需要从头节点一个一个依次遍历找到对应节点,但在进行修改操作时不需要像数组操作那样将修改点后面的所有点移动,只需要对修改点前后节点的next地址进行修改就可以完成。
双向链表
在单链表的基础上我们可以将一块数据内存分成三块,一块存储数据,两块来存储地址,结构如下:
用这样的内存块来实现链表,既可以找到下一个节点的内存块,也可以找到前一个节点的内存块。
初始化
依据以上的理论我们运用双向链表来实现LinkList,在构建之前需要先构建一个Node类:
//用来表示一个节点
public class Node {
Node previous; //上一个节点
Object obj;
Node next; //下一个节点
public Node() {
}
public Node(Node previous, Object obj, Node next) {
super();
this.previous = previous;
this.obj = obj;
this.next = next;
}
public Node getPrevious() {
return previous;
}
public void setPrevious(Node previous) {
this.previous = previous;
}
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
可以看到节点中包括数据内容,前一个和后一个节点,在这个类的基础上我们定义自己的LinkList:
public class MyLinkedList {
private Node first;//第一个节点
private Node last;//最后一个节点
private int size;//链表长度
public int size(){
return size;
}
//检查索引是否越界
private void rangeCheck(int index){
if(index<0||index>=size){
try {
throw new Exception();
} catch (Exception e) {
e.printStackTrace();
}
}
}
//获取指定位置节点
public Node node(int index){
Node temp = null;
if(first!=null){
//如果索引小于size/2,从first向后遍历
if (index < (size >> 1)) {
temp = first;
for(int i=0;i<index;i++){
temp = temp.next;
}
//如果索引大于size/2,从last向前遍历
}else{
temp = last;
for (int i = size - 1; i > index; i--){
temp = temp.previous;
}
}
}
return temp;
}
//获取指定位置元素
public Object get(int index){
rangeCheck(index);
Node temp = node(index);
if(temp!=null){
return temp.obj;
}
return null;
}
}
add方法
与ArrayList对应我们也添加两个add方法:
//在链表的最后添加节点
public void add(Object obj){
//新建一个节点n
Node n = new Node();
if(first==null){
n.setPrevious(null);
n.setObj(obj);
n.setNext(null);
first = n;
last = n;
}else{
//直接往last节点后增加新的节点
//n的前一个节点是原链表的最后一个节点
n.setPrevious(last);
n.setObj(obj);
n.setNext(null);
//原链表最后一个节点的下一个节点为n
last.setNext(n);
将n设置为最后一个节点
last = n;
}
size++;
}
public void add(int index,Object obj){
//获取index处节点
Node temp = node(index);
//新建一个节点
Node newNode = new Node();
newNode.obj = obj;
if(temp!=null){
//获取index-1位置的节点
Node up = temp.previous;
//index-1位置的节点下一个节点为newNode
up.next = newNode;
//newNode的前一个节点为index-1位置的节点
newNode.previous = up;
//newNode的下一个节点为index位置的节点
newNode.next = temp;
//index位置的节点前一个节点为newNode
temp.previous = newNode;
size++;
}
}
remove方法
public void remove(int index){
Node temp = node(index);
if(temp!=null){
Node up = temp.previous;
Node down = temp.next;
up.next = down;
down.previous = up;
size--;
}
}
//删除指定元素
public void remove(Object obj) {
for (int i = 0; i < size; i++) {
if (get(i).equals(obj)) {
remove(i);
}
}
}
以上就是两类List容器的实现方法,对容器不光希望大家可以灵活运用,也希望大家借此来好好阅读下源码,这样既可以帮助我们更好的理解容器,也是对之前所学东西的复习与加强。这一篇就到这里了,下一篇我会进行Map的讲解。
上一篇:菜鸟学习笔记:Java基础篇7(包装类、时间相关类、文件类、异常处理类)
上一篇:菜鸟学习笔记:Java提升篇2(容器下篇——Map、Set、迭代器)