第 12 章 内部类

通过本章可以掌握内部类的主要作用与对象实例化的形势,掌握static内部类的的另一,掌握匿名内部类的定义与使用,掌握Lambda表达式语法,理解方法引用的作用,并且可以利用内建函数式接口实现方法引用,了解链表涉及的目的以及实现结构。

12.1 内部类基本概念

内部类(内部定义普通类、抽象类、接口的统称)是指一种嵌套的结构关系,即在一个类的内部除了属性和方法外还可以继续定义一个类结构,这样就是的程序的结构定义更加灵活。

范例:定义内部类

package cn.demo;
class Outer
{
private String msg="AAAA";
public void fun(){Inner in =new Inner();in.print();}
class Inner{
public void print(){System.out.println(Outer.this.msg);}
}
}

public class JavaDemo
{
public static void main(Stringa args[])
{
Outer out=new Outer();
out.fun();
}
}

本程序从代码上理解上应该难度不大,核心的结构就是在Outer.fun()方法里实例化了内部类的对象,并且利用内部类中的print()方法直接输出了外部类中msg私有成员属性。

提问:内部类的结构较差吗
从类的组成来讲主要就是成员属性与方法,但是此时在一个类的内部有定义若干个类结构,使得程序代码的结构非常混乱,为什么要这样定义。
回答:内部类方便访问私有属性
实质上内部类在整体设计中最大的缺点在于破坏了良好的程序结构,但是其最大的优点在于可以方便的访问外部类中的私有成员,为了证明这一点,下面将之前的范例拆分为两个独立的类结构。

范例:内部类结构拆分,形成两个不同类
package cn.demo;
class Outer
{
private String msg="AAAA";
public void fun(){Innner in=new Inner(this);
in.print();}
public String getMsg(){return this.msg;}
}

class Inner
{
private Outer out;
public Inner(Outer out);
{
this.out.=out;
}
public void print()
{
System.out.prinln(this.out.getMsg());
}
}

public class JavaDemo
{
public static void main(String args[])
{
Outer out=new Outer();
out.fun();
}
}

综合来讲:再没有内部类的情况下,如果要进行外部私有类成员的访问非常麻烦。
        另外,需要读者注意的是,之所以会有内部类,更多的希望是希望某一个类只为单独一个类服务的情况。

12.2 内部类的说明

在内部类的结构中,不仅内部类可以方便访问外部类的私有成员。内部类本省也是一个独立结构,这样在进行普通成员属性访问时,为了明确标记处属性是外部类所提供的,可以采用“外部类.this.属性”的形势进行标注。

范例:外部类访问内部类私有成员

package cn.demo;
class Outer
{
private String msg="AAA";
public void fun()
{
Inner in=new Inner();
in.print();
System.out.println(in.info());
}
class Inner
{
private String info="AAA";
public void print(){System.out.println(Outer.this.msg);}
}
}

public class JavaDemo
{
public static void main(String args[])
{
Outer out=new Outer();
out.fun();
}
}

本程序在内部类中利用Outer.this.msg的形式调用外部类中的私有成员属性,而在外部类中也可以直接利用内部类的对象访问内部私有成员。
        需要注意的是,内部类虽然被外部类所包裹,但是其本省也属于一个完整类,所以也可以直接进行内部类的实例化,可采用以下形式:
外部类.内部类 内部类对象=new外部类().new 内部类();
在此语法格式中,要求必须先获取相应的外部类实例化对象后,才可以利用外部类的实例化对象进行内部类对象实例化操作。
提示:关于内部类的字节码文件名称
当进行内部类源代码编译后,读者会发现有一个Outer$Inner.class字节码对象,其中所使用的标识符“$”在程序中会转变为".",所以内部类全程就是“外部类.内部类”,由于内部类与外部类之间可以直接进行私有成员的访问,这样就必须保证在实例化内部类对象前首先实例化对象类对象。

范例:实例化内部类对象
package cn.demo;
class Outer
{
private String msg="AAA";
class Inner
{
public void print(){System.out.println(Outer.this.msg);}//Outer类中的私有成员属性。
}
}

public class JavaDemo
{
public static void main(String args[])
{
Outer.Inner in=new Outer().new Inner();
in.print();
}
}

本程序在外部实例化内部类对象,由于内部类有可能要进行外部类的私有成员访问,所以在实例化内部类对象之前一定要实例化外部类对象。

提示:内部类私有化
如果说现在一个内部类不希望被其他类所利用,那么可以使用private关键字将这个内部类定义为私有内部类。
范例:内部类私有化
class Outer
{
private String msg="AAAA";
private class Inner
{
public void print(){System.out.println(Outer.this.msg);}
}
}

此时Inner类使用了private定义,表示此类只允许被一个Outer进行使用,private和protedcted定义类的结构只允许出现在内部类声明处。

内部类不仅可以在类中定义,也可以应用在接口和抽象类之中,即可以定义内部的接口或内部抽象类。

范例:定义内部接口
package cn.demo;
interface IChannel
{
public void send(IMessage msg);
interface IMessage{public String getContent();}
}
class ChannelImpl implements IChannel
{
public void send(IMessage msg){System.out.println("发送消息"+msg.getContent());}
class MessageImpl implements IMessage
{
public String getContent(){return "AAA";}
}
}

public class JavaDemo
{
public static void main(String args[])
{
IChannel channel=new ChannelImpl();
channel.send(((Channel)channel).new ,essageImpl());
}
}

本程序利用内部类的形势定义了内部接口,并且分别为外部接口和内部接口定义了各自的子类。由于IMessage是内部接口,所以在定义MessageImpl子类的时候也采用了内部类的定义形式。

范例:在接口中定义内部抽象类

pckage cn.demo;
interface ICannel
{
public void send();
abstract class AbstractMessage{public abstract String getContent();}
}

class ChannelImpl implements IChannel
{
class MessageImpl extends AbstractMessage{public String getContent{return "AAA"}}
public void send()
{
AbstractMessage msg=new MessageImpl();
msg.getContent();
}
}

public static void main(String args[])
{
IChannel channel=new ChannelImpl();
channel.send();
}

本程序在IChannel外部接口内部定义了内部抽象类,在定义ChannelImpl子类的print()方法时,利用内部抽象类的子类为父类对象实例化,实现消息的发送。
        在JDK1.8之后由于接口中可以定义static方法,这样就可以利用内部类的概念,直接在接口中进行该接口子类的定义,并利用static方法返回此接口实例。

范例:接口子类定义为自身内部类
package cn.demo;
interface IChannel
{
public void send();
class ChannelImpl implements IChannel
{
public void send();
{
...
}

}
public static IChannel getInstance()
{
return new channelImpl();
}
}

public class JavaDemo
{
public static void main(String args[])
{
IChannel chanel=IChannel.getinstance();
channel.send();
}
}

本程序通过IChannel接口时直接在内部定义了其实现子类,同时为了方便用户获取接口实例,使用static定义了一个静态方法,这样用户就可以在不关系子类的前提下直接使用接口对象。

12.3 static定义内部类

在进行内部类定义的时候,也可以通过static关键字来定义,此时的内部类不再受到外部类实例化对象的影响,所以等同于是一个外部类,内部类的名称为外部类.内部类。使用static定义的内部类只能够调用外部类中static定义的结构,并且在进行内部类实例化的时候也不再需要先获取外部类实例化对象,static内部类对象实例化格式如下:
外部类.内部类 内部类对象=new 外部类.内部类();
范例:使用static定义内部类

package cn.demo;
class Outer
{
private static finale String MSG="AAA";
static class Inner
{
public class Inner
{
public void print(){System.out.println(Outer.MSG);}
}
}
}

public class JavaDemo
{
public static void main(String args[])
{
Outer.Inner in=new Outer.Inner();
in.print();
}
}

本程序在Outer类的内部使用了static定义了Inner内部类,这样内部类就成为了一个独立的外部类,在外部实例化对象时内部类的完整名称为Outer.Inner.

范例:使用static定义内部接口

package cn.demo;
interface IMessageWarp
{
static intreface IMessage
{
public String getContent();
}
static interface IChannel
{
public boolean connect();
}

public static void send (IMessage msg,IChannel channel)
{
if(channel.connect(()){System.out.prinln(msg.getConnect());}
else
{
...
}
}
}

class DefaultMessage implements IMessageWarp.IMessage
{
public String getContent(){}
}

class NetChannel implements IMessageWarp.IChannel
{
public boolean connect(){return true;}
}

public static void main(String args[])
{
IMessageWarp.send(new DefaultMessage(),new NetChannel());
}

本程序在IMessageWarp接口中定义了两个“外部接口”:IMessageWarp.IMessage(消息内容)、IMessageWarp.IChannel(消息发送通道),随后在外部分别实现了这两个内部接口,以实现消息的发送。

注意:实例化内部类对象的格式比较。
对于现在实例化内部类的操作已经给出了两种形式,分别如下:
格式1(非static定义内部类);外部类.内部类 内部类对象=new 外部类().new 内部类().
格式2(static定义内部类);外部类.内部类 内部类对象=new外部类.内部类().
通过这两种形式可以发现,使用了static定义的内部类,其完整的名称就是外部类.内部类,在实例化的时候也不需要先实例化外部类在实例化内部类了。

12.4 方法中定义内部类

内部类理论上可以在类的任意位置上进行定义,这就包括代码块中或者普通方法中,而在实际开发过程中,在普通方法里面定义内部类的情况比较少见。

范例:在方法中定义内部类

package cn.demo;
class Outer
{
private String msg="AAAA";
public void fun(long time)
{
class Inner
{
public void print(){

}
}

new Inner().print();
}
}

public class JavaDemo
{
public static void main(String args[])
{
new Outer().fun(1111L);
}
}

本程序在Outer.fun()方法中定义了内部类Inner(),并且Inner类内部类中实现了内部类Inner,并且Inner内部类中实现了外部类中成员属性与fun()方法中的参数访问。

提示:内部类中访问方法参数
在上述程序中可以发现,方法定义的参数可以直接被内部类访问,这一特点在JDK1.8之后才开始支持的。但是在JDK1.8之前,如果方法中定义的内部类要想访问参数或者局部变量,那么就需要使用final关键字进行定义。

范例:JDK1.8以前方法中定义内部类
class Outer
{
private String msg="AAA";
public void fun(final long time)
{
final String info="AAA";
class Inner
{
public void print()
{
...
}
}

new Inner().print();
}
}

12.5 匿名内部类

在一个接口或抽象类定义完成后,在使用前都需要定义专门的子类,随后利用子类对象的向上转型才可以使用接口或抽象类。但是在很多时候某些子类可能只使用一次,那么单独为其创建一个类文件就会非常浪费,此时可以利用匿名内部类的概念来解决此问题。

范例:使用匿名内部类

package cn.demo;
interface IMessage{

public void send(String str);
}

public class JavaDemo
{
public static void main(String args[])
{
IMessage msg=new IMessage()
{
public void send(String str)
{
public void send(String str){System.out.println(str);}
}
};
msg.send("AAA");
}
}

本程序利用匿名内部类直接在接口中实现了自身,这样的操作形式适用于接口只有一个子类的时候,并且也可以对外部调用处隐藏子类。

12.6 Lambda表达式

Lambda表达式是JDK1.8中引入的重要技术特征。所谓的Lambda表达式,是指应用在SAM(Single Abstract Method,含有一个抽象方法的接口)环境下的一种简化定义形式,用于解决匿名内部类的定义复杂问题,在Java中Lambda表达式的基本语法形式如下。
        

定义方法体(参数,参数)->方法体
直接返回结果(参数,参数)->语句

在给定的格式中,参数与要覆写的抽象方法的参数对应,抽象方法的具体操作就通过方法体来进行定义。

范例:编写第一个Lambda表达式

package cn.demo;
interface IMessage{public void send(String str);}
public class JavaDemo
{
public static void main(String args[])
{
IMessage msg=(str)->{...};
}

msg.send("AAAA");
}

本程序利用Lambda表达式定义了IMessage接口的实现类,可以发现Lambda表达式进一步简化了内部类的定义结构。

提示:Lambda单个方法定义
在进行Lambda表达式定义的过程中,如果要实现的方法体只有一行,则可以省略{}.
范例:省略{}定义Lambda表达式
public class JavaDemo
{
public static void main(String args[])
{
IMessage msg=(str)->System.out.println("发送消息"+str);
msg.send("AAA");
}
}

在Lambda表达式中已经明确要求Lambda是应用在接口上的一种操作,并且接口中只允许定义有一个抽象方法。但是在一个项目开发中往往会定义大量的接口,而为了分辨出Lambda表达式的使用接口,可以再接口上使用@FunctionInterface注解声明,这样表示此为函数式接口,里面只允许定义一个抽象方法。

范例:使用@FunctionalInterface注解

@FunctionalInterface
interface IMessage
{
public void send(String str);
}

从理论上来讲,如果一个接口只有一个抽象方法,写与不写@FunctionalInterface注解是没有任何区别的,但从标准来讲,还是建议写上此注解。同时需要注意的是,在函数式接口中依然可以定义普通方法与静态方法。

范例:定义单行返回数据

package cn.demo;
@FunctionalInterface
interface IMath
{
public int add(int x,int y);
}

public class JavaDemo
{
public static void main(String args[])
{
IMath math=(t1,t2)->t1+t2;
System.out.println(math.add(10,20));
}
}

本程序由于只是简单地进行了两个数字的加法操作,所以直接在方法体处编写语句即可将计算的结果返回。

12.7 方法引用

在Java中利用对象的引用传递可以实现不同的对象名称操作同一块堆内存空间的操作,而从JDK1.8开始,方法也支持引用操作,这样就相当于为方法定义了别名。方法的引用形式一般有以下4中。
1 引用静态方法:类名称::static方法名称。
2 引用某个对象的方法:实例化对象::普通方法
3 引用特点类型的方法:特定类::普通方法。
4 引用构造方法::类名称::new

范例:引用静态方法

本次将引用在String类里的valueOf()静态方法(public static String valueOf(int x))
package cn.demo;
@FunctionInterface
interface IFunction<P,R>{public R changeP,R);}

public class JavaDemo
{
public static void main(String args[])
{
IFunction<Interger,String> fun=String::valueOf;
String str=fun.change(100);
System.out.println(str.length());
}
}

本程序定义了一个IFunction的函数式接口,随后利用方法引用的概念引用了String.valueOf()方法,并且利用此方法的功能将int型常量转为String型对象。

范例:引用普通方法

本次引用String类中的字符串转大写的方法:public String toUpperCase().

package cn.demo;
@FunctionalInterface<R>{public R upper();}
public class JavaDemo
{
public static void main(String args[])
{
IFunction<String> fun="AAAA"::toUpperCase;
System.out.println(fun.upper());
}
}

String类中提供的toUpperCase()方法一般都需要通过String类的实例化对象才可以调用,所以本程序使用实例化对象引用了类中的普通方法("AAAA"::toUpperCase)为IFunction接口的upper(),即调用upper()方法就可以实现toUpperCase()方法的执行结果。
        在进行方法引用的过程中还有另一种形式的引用,她需要特定类的对象支持,真诚工行请款项GIA如果使用了类::方法,引用的一定是类中的静态方法,但是这种形式也可以引用普通方法(字符串2对象),也就是说真要引用这个方法就需要准备两个参数。

范例:引用特定类的普通方法
本次将引用String类的字符串大小比较方法:public int compareTo(String anotherString)

package cn.demo;
@FunctionInterface
interface IFunction<P>
{
public int compare(P p1,P p2)l
}

public class JavaDemo
{
public static void main(String args[])
{
IFunction<String>fun=String::compareTo;
System.out.println(fun.compare("AAA","aaa"));
}
}

本程序直接引用了String类中的compareTo()方法,由于此方法调用时需要通过指定对象才可以,所以在使用引用方法compare()的时候就必须传递两个参数,与之前的引用操作相比,方法引用前不再需要定义具体的类对象,而是可以理解为将需要调用方法的对象作为参数u进行了传递。

范例:引用构造方法

package cn.demo;
class Person
{
private String name;
private int age;
public Person(String name,int age)
{
this.name=name;
this.age=age;
}
public String toString(){return ""+this.name+this.age;}
}

@FunctionalInterface
interface IFunction<R>
{
public R create(String s,int a);
}

public class JavaDemo
{
public static void main(String args[])
{
IFunction<Person>fun=Person::new;
System.out.println(fun.create("张三",20));
}
}

本程序中使用了IFunction.create()方法实现了Perosn类中双参构造方法的引用,所以在调用此方法时就必须按照Person类提供的构造方法形式传递指定的参数。构造方法的引用在实际开发过程中可以实现类中构造方法的对外隐藏,更加彰显了对象的封装性。

12.8 内建函数式接口

在方法引用的操作过程中,读者可以发现,不管如何进行曹组,对于可能出现的函数接口的方法最多只有4类:有参数有返回值、有参数无返回值、无参数有返回值、判断正价。所以为了简化开发者的定义以及操作的同意,从JDK1.8开始提供了一个新的开发包:java.util.function,在此包中提供了许多内置的函数式接口,下面通过具体的案例来分析4个核心函数式接口的使用。

提示:java.util.function包中存在大量类似功能的其他接口。

(1)功能型函数式接口:该接口的主要功能是进行指定参数的接受并且可以返回结果。
@FunctionalInterface
public interface Function<T,R>
{
public R apply(T t);
}

范例:使用功能型函数式接口

本次将引用String类中判断是否以指定字符串开头的方法:public boolean startsWith(String str)
package cn.demo;
import java.util.function.*;
public class JavaDemo
{
public static void main(String args[])
{
Function<String,Boolean>fun="aAAA"::startWith;
System.out.println(fun.apply("**");
}
}

如果要使用功能型函数式接口,必须保证有一个输入参数并且由返回值,由于映射的是String类的startsWith()方法,所以此方法使用时必须传入参数(String型),同时返回一个判断结果(boolean型)。
        (2)消费型函数式接口:该接口的主要功能时进行参数的接受与处理,但是不会有返回结果。

@FunctionalInterface
public interface Consumer<T>{public void accept(T t);}
范例:使用消费星函数接口
package cn.demo;
import java.util.function.*;
public class JavaDemo
{
public static void main(String args[])
{
Consumer<String>con=System.out::println;
con.accept("AAAA");
}
}

本程序利用消费型函数式接口接受了System.out.println()方法的引用,此方法定义中需要接收一个String型数据,但是不会返回任何结果。

(3)供给型函数接口:该接口的只要功能是方法不需要接收参数,并且可以进行数据返回。

@FunctionInterface
public interface Supplier<T>
{
public T get();
}

范例:使用供给形函数式接口

本次将引用String类中的字符串转小写方法:public String toLowerCase()
package cb.demo;
import java.util.function.*;
public class JavaDemo
{
public static void main(String args[])
{
Supplier<String>sup="AAA"::toLowerCase;
System.out.println(sup.get());
}
}

本程序使用了供给形函数式接口,此接口上不需要接收参数,所以直接利用String类的实例化对象引用了toLowerCase()方法,当调用了get()方法之后可以实现大写转换操作。
(4)使用断言型函数式接口

本次将使用String类中的忽略大小写比较方法:public boolean equalsIgnoreCase(String str);
package cn.demo;
import java.util.function.*;
public class JavaDemo
{
public static void main(String args[])
{
Predicate<String>pre="AAAA".equalsIgnoreCase;
System.out.println(pre.test("AAA"));
}
}

12.9 链表

在项目开发中数组是一个重要的逻辑组成,在项目中可以用于描述多的概念,例如,一个人有多本书,一个国家有多个省份等。传统数组中最大的缺陷在于其一旦声明则长度固定,不便于程序开发,而要想解决这一缺陷,就可以利用链表数据结构来实现。

提示:以下所讲解的链表实现代码有一定理解难度

链表(很多时候统称为集合)是一个重要的数据结构实现,本身的实现较为复杂,涉及的代码也较多,本章会将重要的代码进行展示,数据结构在Java中也是有支持类库的。
        链表(动态数组)的本质是利用对象引用的逻辑关系来实现类似于数组的数据存储逻辑,一个链表上有点若干个节点(Node)组成,每一个节点依靠对上一个节点的引用形成一个链的形式。数组本身是需要进行多个数据的信息保存,但是数据本身并不能够描述出彼此间的先后顺序,所以就需要将数据包装在节点(Node)中。每一个节点除了要保存数据心外,一定还要保存有下一个节点(Node)的引用,而在链表中会保存一系列的节点对象。
        在进行Node类设计时,为了避免程序开发中可能出现ClassCastException安全隐患,对于保存的数据类型都用泛型进行定义,这样可以保证在一个链表中的数据类型统一。

范例:直接使用Node类存放多个数据

package cn.demo;
class Node<E>
{
private  E data;
private Node<E>next;
public Node(E data){this.data=data;}
public E getData()
{
return this.data;
}
public void setNext(Node<E>next){this.nect=next;}
public Node<E>getNext(){return this.next;}
}

public class LinkDemo
{
public stctic void main(String args[])
{
Node<String>n1=new Node<String>("火车头");
Node<String>n2=new Node<String>("车厢一");
Node<String>n3=new Node<String>("车厢2");
Node<String>n4=new Node<String>("车厢3");

Node<String>n5=new Node<String>("车厢4");
n1.setNext(n2);
n2.setNext(n3);
n3.setNext(n4);
n4.setNext(n5);
printNode(n1);

}
}

public static void printNode(Node<?>node)
{
if(node!-null)
{
System.out.println(node.getData()+",");
printNode(node.getNext());
}
}

本程序利用节点的引用关系,将若干个Node类的对象串联在一起,这样在进行数据获取时只需根据引用逻辑,从第一个节点利用递归以逻辑向后一支输出即可。
        代码分析到处子,读者对于Node类应该有所了解,但是如果所有的Node类的对象的创建以及引用关系都由调用者来处的话,这样没有任何意义。因为Node类的设计是为了链表来服务端,链表本质是一个动态数组,既然是数组结构,那么开发者是不需要关注内部如何存储,开发者所关系的只是数据的保存与获取,所以在实际过程中,链表需要对外部封装Node类的实现与操作细节。
在图中所示的结构中,为了方便链表类中对于数据的保存,将Node类设计为了一个内部类的形势,目的是让Node类只为LinkImpl一个类服务,这样就可以形成以下的链表基本模型。

范例:定义链表基本模型

interface ILink{}
class LinkImpl<E>implements ILink<E>
{
private class node<E>{
private E data;
private Node<E>next;
public Node(E data){this.data=data;}
}
}

本程序在LinkImpl子类中定义了Node内部类,为了防止程序其他类使用Node类,所以采用private关键字进行封装,并利用Node类实现引用关系的处理。在链表的整体视线中会依据ILink接口的定义对Node类的功能进行扩充,在链表的整体式险种,ILink接口的主要方法如下。

12.9.1 链表数据增加

链表在进行定义的时候使用了泛型技术,这样就可以保证每个链表多保存的相同类型的数据,这样即可以避免ClassCastEception安全隐患,又可以保证在进行对象比较时的数据类型统一。
        链表是多个节点的集合,所以在链表类中为了可以方便的进行所有节点的操作,则需要进行根节点的保存,每一次增加的新节点都要按照顺序保存在最后一个节点后进行存储:
(1)【ILink】在ILike接口中定义数据增加方法。
向链表中进行数据的存储,每个链表所保存的数据类型相同,不允许保存null数据。
public void add(E e);
(2)【Link.Node】没当进行链表数据增加时都需要创建新的Node类对象,并且需要根据引用关系保存Node类对象,此操作可以交由Node类来完成,所以Node类中追加节点保存的方法。
public void addNode(node<E> new Node)
{
if(this.nex==null){this.next=newNode;}else{this.next.addNode(newNode);}
}

(3)【LinkImpl】链表实现子类中定义根节点对象。
priavte Node<E>root;
(4)【LinkImpl】在LinkImpl子类中覆写ILink接口中定义的方法
@Override
public void add(E e)
{
if(e==null)return;
Node<E> new Node=new Node<E>(e);
if(this.root==null)
{
this.root=newNode;
}else{this.root.addNode(newNode);}
}

在LinkImpl子类中主要功能是将要保存在链表中的数据包装在Node类对象中,这样就可以利用Node类中所提供的next属性来定义不同Node类对象间的先后关系。在链表视线中最为重要的就是根节点的保存,即通过根节点可以实现所有后续节点的处理,本程序降低一个保存的节点作为根节点。
(5)【测试类】在主类中进行链表的保存

public class LinkDemo
{
public static void main(String args[])
{
ILink<String> link=new LinkImpl<String>();
link.add("AAAA");
link.add("AAAAB");
link.add("AAAAC");
}
}

在客户端使用时可以利用子类对象的向上转型为ILink父接口实例化,这样就可以直接调用add()方法进行链表数据存储,由于链表实现了所有节点的创建与引用处理,所以客户端不必再关系Node类的操作。

12.9.2 获取链表元素个数

链表中往往会保存大量的数据内容,同时链表的本质有相当于一个数组,那么为了可以准确的获取数据的个数,就需要在链表中进行数据的统计操作。
(1)ILink在ILink接口中定义一个size()方法用于返回数据保存个数。
public int size();
(2)【LinkImpl】在LinkImpl子类中定义一个新的成员属性用于进行元素个数的统计。
private int count;
(3)【LinkImpl】在元素保存成功时可以进行count属性的自增处理,修改add()方法
@Override
public void add(E e)
{
this.count++;
}
(4)【LinkImpl】在LinkImpl子类中覆写size()方法,返回count成员属性
@Override
public int size(){return this.count;}
(5)【测试类】在主类中调用size()方法
public class LinkDemo
{
public static void main(String args[])
{
ILink<String>link=new LinkImpl<String>();
System.out.println("数据保存前链表元素个数"+link.size());
link.add("AAAA");
link.add("BBBB");
link.add("cccc");
System.out.println("数据保存后链表元素个数"+link.size());
}
}

本程序分别在链表数据保存前和保存后进行了数据个数的统计。

12.9.3 空集合判断

链表中可以进行若干数据的保存,在链表对象实例化完毕但还未进行数据保存时,该链表就属于一个空集合,那么就可以在链表中追加一个空集合的判断。
(1)【ILink】在ILink接口中定义一个新的方法,用于判断当前集合是否为空集合。
public boolean isEmpty();
(2)【LinkImpl】在LinkImpl子类中覆写isEmpy()方法。
@Override
public boolean isEmpty()
{
return this.count==0;
}
本程序通过判断集合长度是都为0的方式来检测当前集合是否为空集合,实际上也可以通过判断根元素是否为空的形式来验证。

12.9.4 返回链表数据

链表中多有数据通过Node封装后实现了动态保存,并且取消了数组长度的限制。但是保存在链表中的数据也许要被外部获取,那么此时就可以利用数组的形势返回链表中的保存数据。考虑到此功能的通用性,返回数组类型应该为Object。在进行链表数据获取时,应该根据当前链表所保存的集合长度开辟相应的数组,随后利用索引的方法从链表中获取相应的数据并将其保存在数组中。
(1)【ILink】在ILink接口中追加方法用于返回链表数据。
public Object[]toArray();
(2)【LinkImpl】在LinkImpl子类中定义两个成员属性,用于返回数组声明与数组索引控制。
private int foot;
private Object[] returnDate;
(3)【LinkImpl.Node】在Node类中追加新的方法,通过递归的形式将链表中的数据保存在数组中。

public void toArrayNode()
{
LinkImpl.this.returnData[LinkImpl.this.foot++]=this.data;
if(this.next!=null){this.next.toArrayNode();}
}
(4)【LinkImpl】覆写ILink接口中的toArray()方法。
@Override
public Object[] toArray()
{
if(this.isEmpy()){throw new NullPointerException("集合内容为空");}
this.foot=0;
this.returnData=new Object[this.count];
this.root.toArrayNode();
return this.returnData;
}
(5)【测试类】编写测试程序调用toArray()方法
public class LinkDemo
{
public static void main(String args[])
{
ILink<String>link=new LinkImpl<String>();
link.add("AAA");
link.add("AAAB");
link.add("AAAC");
Object results[]=link.toArray();
for(Object obj:results)
{
String str=(String)obj;
System.out.println(str+"、");
}
}
}

本程序通过链表中的toArray()方法可以保存在链表中的数据全部取出,就可以利用foreach实现内容打印。
提示:关于集合的常见操作
实际上随着读者开发经验的不断提神,慢慢就会发现对于链表这类集合的数据操作,最为常见的就是保存数据与获取数据,并且不会受到长度的限制。

12.9.5 根据索引获取数据

传统数组和链表都是基于顺序式的形势实现了数据的保存,所以链表也可以利用索引的形势通过递归获取指定数据。
(1)【ILink】在ILink接口中定义新的方法可以根据索引获取数据
public E get(int index)
{

}
(2)【LinkImpl.Node】在Node类中追加索引获取数据的方法,此时可以利用LinkImpl类中的foot进行索引判断。
public E getNode(int index){if(LinkImpl.this.foot++==index)return this.data;}else{return this.next.getNode(index);}
(3)【LinkImpl】在LinkImpl子类中覆写get()方法
@Override
public E get(int index)
{
if(index>=this.count)throw new ArrayIndexOutOfBoundsException("不正确的索引方式");
}
(4)【测试类】编写测试方法,调用get()方法
public class LinkDemo
{
public static void main(String args[])
{
ILink<String>link=new LinkImpl<String>();
link.add("AAAA");
link.add("AAAA");
link.add("AAAA");
System.out.println(Ilink.get(1));

}
}

本程序在get()方法中由于存在索引的检查机制,所以一旦使用了不正确的索引将会产生相应的异常提示给用于。
提示:关于时间复杂福问题
衡量程序算法的优劣有两个重要的参考条件:时间复杂度和空间复杂度。在本程序中要获取一个指定索引的事件复杂度为n,即有n个元素,那么本次递归调用就有肯呢个出现n次,而数组根据索引查询的时间复杂度为1,可以直接进行定位。按照这个方式来分析,当链表中保存的数据越多,那么执行的性能就可能越慢,所以一个可供调用的链表中必然考虑这些性能,而这些也是数据结构学习中需要考虑的问题,幸运的是Java提供专门的类库框架帮助开发者提高开发效率并简化开发难度。

12.9.6 修改链表数据

链表中的数据由于存在foot这个成员变量就可以通过索引的形势来进行操作,利用索引可以实现内容的修改。
(1)【ILink】在ILink接口中追加数据修改方法
public void set(int index,E data);
(2)【LinkImpl.Node】在Node类中增加一个索引数据修改的方法
public void setNode(int index,E data)
{
if(LinkImpl.this.foot++==index){this.data=data;}
else{this.next.setNode(index,data);}
}

(3)【LinkImpl】在LinkImpl子类中覆写set()方法
@Override
public voise set(int index,E data)
{
if(index>=this.count)throw new ArrayIndexOutBoundsException("不正确数据索引");
this.foot=0;
this.root.setNode(index,data);
}
(4)【测试类】编写测试程序,调用get()方法。
public class LinkDemo
{
public static void main(String args[])
{
ILink<String>link=new LinkImpl<String>();
link.add("AAA");
link.add("AAAB");
link.add("AAAC");
link.set(1,"111");
System.out.println(link.get(1));
}
}

本程序利用了set()方法修改了指定索引的内容,随后利用get()方法获取索引数据,实质上set()与get()两个方法的实现原理相同。

12.9.7 数据内容查询

在一个集合里面往往往往会保存大量的数据,有时需要判断某个数据是否存在,这时就可以利用迭代的方法进行对象比较(euqals()方法)来完成判断。
(1)【ILink】在ILink接口中定义数据查询方法。
public boolean contains(E data);
(2)【Linkimpl.Node】节点递归处理以及数据判断在Node类中完成:
public boolean containsNode(E data)
{
if(this.data.equals(data))return trus;
else{if(this.next==null)return false;else {return this.next.containsNode(data);}}
}

(3)【LinkImpl】在LinkImpl子类中覆写contains()方法。
@Override
public boolean contains(E data)
{
if(data==null)return false;
return this.root.ccontainsNode(data);
}

(4)【测试类】编写测试类测试数据查找

public class LinkDemo
{
public static void main(Stringa rgs[])
{
ILink<String >link=new LinkImpl<String>();
link.add("AAAA");
link.add("AAAB");
link.add("AAAC");
System.out.println(link.contains("AAA"));
System.out.println(link.contains("张老师"));
}
执行结果true,false,在调用contains()方法时,利用的是equals()方法实现对象比较处理。如果现在在链表中保存的是自定义对象,则对象所在的类一定要覆写equals()方法。

12.9.8 删除链表数据

链表作为动态数组除了可以任意的进行长度的扩充之外,还可以实现指定数据的删除操作,由于链表作为一个Node对象的集合,所以在删除数据时需要考虑一下两种情况。
情况1:要删除的数据是根节点。
由于根节点在链表类(LinkImpl)中保存的是成员属性,一旦要删除的是根节点内容,则可以将第二个节点(根节点.next)作为根节点,操作如图:

情况2:要删除的是子节点
所有子节点的控制全部都是由Node类实现的,所以此时可以直接在Node类中修改要删除节点的引用,修改的原则为删除节点的上一节点.next=删除.next:
(1)【ILink】在ILink接口中增加一个数据删除方法。
public void remove(E data);
(2)【LinkImpl.Node】
public void removeNode(Node previous,E data)
{
if(this.data.equals(data)){previous.next=this.next;}else
{
if(this.next!=null){this.next.removeNode(this,data);}
}
}

(3)【LinkImpl】在LinkImpl子类中实现节点的删除
@Override
public void remove(E data)
{
if(this.contains(data))
{
if(this.root.data.equls(data)){this.root=this.root.next;}
else{this.root.next.removeNode(this.root,data);}
}
this.count--;
}

LinkImpl子类需要进行根节点的存储,所以对于根节点的数据删除将由LinkImpl子类来完成,而对于子节点的删除将交由Node.removeNode()方法处理。需要注意的是,元素一旦成功删除后,需要对count成员属性进行修改。

12.9.9 清空链表数据

链表中的所有数据都是根据根节点进行的引用保存,如果想要进行链表数据的整体删除,直接删除掉根节点的数据即可。
(1)【ILink】在ILink接口中追加清空链表的方法
public void clean();
(2)【LinkImpl】在LinkImpl子类中覆写clean()方法。
@Override
public void clean()
{
this.root=null;
this.count=0;
}

12.10 综合案例:宠物商店

面向对象的程序设计可以实现生活概念的程序抽象化,下面应用面向对象的程序设计解决一个实际问题。现在在假设有一个宠物商店,在这个商店里会出售各种从无供用户进行选择,现在要求通过程序逻辑的描述实现宠物商品的商家、下家、关键字模糊查询的功能。
        分析:在这样一个程序设计中要求中会有许多宠物出现,而宠物店针对宠物的上架、下架与查询信息应该是一句接口来实现的。由于可以再一个宠物店中保存多种宠物信息,此时可以利用链表的方式:
(1)创建宠物接口:IPet
interface IPet
{
public String getName();
public String getColor();
}
(2)宠物商店与宠物的接口标准有关,并不关系宠物的具体子类,所以此时可以直接创建PetShop类
class PetShop
{
private ILink<IPet>allPets=new LinkImpl<IPet>();
public void add(IPet pet){this.allPets.add(pet);}
public void delete(IPet pet){this.allPets.remove(pet);}
public ILink<IPet>search(String keyword)
{
ILink<IPet>searchResult=new LinkImpl<IPet>();
Object<IPet>searchResult=this.allPets.toArray();
if(result!=null)
{
for(obj!=null)
{
IPet pet=(IPet)obj;
if(pet.getName().contains(keyword)||pet.getColor().contains(keyword))
{
searchResult.add(pet);
}
}
}
}
}

(3)根据IPet宠物标准定义宠物猫和狗,为保证链表中contains()、remove()方法方法可以正常使用,需要覆写equals()方法
 

宠物猫(cat)
class Cat implements IPet
{
private String name;
private String color;
public Cat(String name,String color)
{
this.name=name;
this.color=color;
}
@Override
pubic String getName(){return this.name;}
@Override
public String getColor(){return this.color;}
@Override
public boolean equals(Object obj)
{
if(obj==null)return false;
if(!(obj instanceof Cat))return false;
if(this==obj)return true;
Cat cat=(Cat)obj;
return this.name.equals(cat.name)&&this.color.equals(cat.color);
}
@Override
public String toString(){return "宠物猫"+this.name+this.color;}
}
class Cat implements IPet
{
private String name;
private String color;
public Cat(String name,String color)
{
this.name=name;
this.color=color;
}
@Override
pubic String getName(){return this.name;}
@Override
public String getColor(){return this.color;}
@Override
public boolean equals(Object obj)
{
if(obj==null)return false;
if(!(obj instanceof Cat))return false;
if(this==obj)return true;
Cat cat=(Cat)obj;
return this.name.equals(cat.name)&&this.color.equals(cat.color);
}
@Override
public String toString(){return "宠物狗"+this.name+this.color;}
}

(4)编写主类进行代码测试
public class PetDemo
{
public static void main(String args[])
{
PetShop shop=new PetShop();
shop.add(new Dog("11","22"));
shop.add(new Cat("11","22"));
Object result[]=shop.search("黄").toArray();
for(Object obj:result){System.out.println(obj);}
}
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值