第六章面向对象(2)

Java学习笔记

面向对象(2)

Java8 增强的包装类

  • Java的8种基本数据类型不具备对象的特性。为解决基本数据类型变量不能当成Object类型变量使用的问题,Java提供了包装类的概念,为8种基本数据类型分别定义了相应的引用类型,并称之为基本数据类型的包装类。
基本数据类型包装类
byteByte
shortShort
intInteger
longLong
charCharacter
floatFloat
doubleDouble
booleanBoolean
  • Java提供的基本类型变量和包装类对象之间的转换有点繁琐,JDK1.5 提供了自动装箱(Autoboxing)和自动拆箱(AutoUnboxing)功能。所谓自动装箱,就是可以把一个基本类型变量直接赋给对应的包装类变量,或者赋给Object变量(Object是所有类的父类,子类对象可以直接赋给父类变量);自动拆箱则与之相反,允许直接把包装类对象直接赋给一个对应的基本类型变量。
public class AutoBoxingUnboxing {
    public static void main(String[] args)
    {
        //直接把一个基本类型变量赋给Integer对象
        Integer inObj = 5;
        Object boolObj = true;
        int it = inObj;

        if(boolObj instanceof Boolean)
        {
            boolean b = (Boolean)boolObj;
            System.out.println(b);
        }
    }
}
  • 当JDK提供了自动装箱与自动拆箱功能够,大大简化了基本类型变量和包装类对象之间的转化过程。值得指出的是,进行自动装箱和自动拆箱时必须注意类型匹配,例如Integer只能自动拆箱成int类型变量,不要试图拆箱成boolean类型变量;与之类似的是,int类型变量只能自动装箱成Integer对象(即使赋给Object类型变量,那也只是利用了Java的自动向上转型特性),不要试图装箱成Boolean类型对象。
  • 包装类提供了两种方式,把字符串类型的值转化为基本类型的值
    • parseXxx(String s)静态方法
    • valueOf(String s)静态方法
public class PrimitivetoString {
    public static void main(String[] args)
    {
        String intStr = "123";

        int it1 = Integer.parseInt(intStr);
        int it2 = Integer.valueOf(intStr);

        System.out.println(it2);
        String floatStr = "4.56";

        float ft1 = Float.parseFloat(floatStr);
        float ft2 = Float.valueOf(floatStr);
        System.out.println(ft2);

        String dbStr = String.valueOf(3.344);
        System.out.println(dbStr);

        String boolstr = String.valueOf(true);
        System.out.println(boolstr.toUpperCase());
    }
}
  • 如果希望把基本类型变量转换为字符串,还有一种更简单的方法:将基本类型变量和""进行连接运算,系统会自动把基本类型变量转换为字符串。
// intStr 的值为"5"
String intStr = 5+"";
  • 虽然包装类型的变量是引用数据类型,但包装类的实例可以与数值类型的值进行比较,这种比较是直接取出包装类实例所包装的数值来进行比较的。
Integer a = Integer.valueOf(6);
System.out.println("6的包装类实例是否大于5.0"+(a>5.0));
  • java8 中再次增强了包装类的功能。其中一个重要的增强就是支持无符号算数运算,为Integer,Long增加了如下方法:
static String toUnsignedString(int/long i):该方法用于将指定的整数转换为无符号字符串
static String toUnsignedString(int/long i, int radix):再额外指定进制
static xxx parseUnsignedXxx(String s):将指定字符串解析成无符号整数,xxx代表调用类的基本类型
static xxx parseUnsignedXxx(String s, int radix)
static int compareUnsigned(xxx x, xxx y):该方法将x、y两个整数转换为无符号整数比较大小
static xxx divideUnsigned(xxx dividend, xxx divisor):该方法将x、y两个整数转换为无符号整数后计算他们相除的商
static xxx remainderUnsigned(xxx dividend, xxx divisor):计算余数

处理对象

  • Java对象都是Object类的实例,都可以直接调用该类中定义的方法,这些方法提供了处理Java对象的通用方法
打印对象和toString方法
package PrintObject;
class Person
{
    private String name;
    public Person(String name)
    {
        this.name = name;
    }
}

public class PrintObject {
    public static void main(String[] args)
    {
        Person p = new Person("lancibe");
        System.out.println(p);
    }
}
  • 运行看到如下结果:
PrintObject.Person@6ff3c5b5
  • @符号后面的8位十六进制数字可能发生改变。println方法只能在控制台输出字符串,而Person实例是一个内存中的对象,怎么能直接转换为字符串输出呢。当时用该方法输出Person实例其实输出的是Person对象的toString()方法的返回值。也就是说,下面代码效果完全一样。
System.out.println(p);
System.out.println(p.toString());
  • toString()方法是Object类里的一个实例方法,所有的Java类都是Object类的子类,所以所有对象都具有toString()方法。不仅如此,所有的Java对象都可以和字符串进行连接运算,实际上也是调用了该实例的toString()方法的返回值与字符串进行连接运算。
  • 这个方法是一个特殊的方法,他是一个“自我描述”的方法,该“方法”通常用于实现这样的功能:当程序员直接打印该对象时,系统将会输出该对象的“自我描述信息”用以告诉外界该对象的状态信息。他的输出结果是类名+@+hashCode值,但他不能真正实现自我描述功能,因此如果用户需要自定义类能实现该功能,则需要重写该方法。
== 和 equals 方法
  • java程序中测试两个变量是否相等有两种方式:一种是利用 == 运算符,另一种是利用 equals() 方法。当使用==时,如果两个变量都是基本类型且都是数值类型(注意不一定要求数据类型严格相同),则只要两个变量的值相等,就将返回true。
  • 但对于引用类型的变量,只有当他们指向同一个对象时,==判断才会返回true。==不可用于比较类型上没有父子关系的两个对象。
public class EqualTest {
    public static void main(String[] args)
    {
        int it = 65;
        float fl = 65.0f;
        System.out.println("65和65.0f是否相等?"+(it==fl));

        char ch = 'A';
        System.out.println("65和A是否相等?"+(it==ch));
        String str1 = new String("hello");
        String str2 = new String("hello");

        System.out.println("str1和str2是否相等?"+(str1 == str2));
        System.out.println("str1是否equals str2?"+(str1.equals(str2)));

        //由于java.lang.String与EqualTest类没有继承关系,所以下面语句导致编译错误
        //System.out.println("hello" == new EqualTest());
    }
}
  • 对于str1和str2,因为他们都是引用类型变量,他们分别指向两个通过new关键字创建的String对象,因此str1和str2并不相等。
  • 当Java程序直接使用型如"hello"的字符串直接量,JVM将会使用常量池来管理这些字符串;当使用new String("hello");时,JVM会先使用常量池里的"hello"直接量,再调用String类的构造器来创建一个新的String类对象,新创建的String对象被保存在堆内存中。换句话说,new String("hello");一共产生了两个字符串对象。
public class StringCompareTest {
    public static void main(String[] args)
    {
        //s1直接引用常量池中的"lancibe"
        String s1 = "lancibe";
        String s2 = "lan";
        String s3 = "cibe";
        //s4后面的字符串值可以在编译时就确定下来
        //s4直接引用常量池的"lancibe"
        String s4 = "lan"+"cibe";
        //s5后面的字符串值可以在编译时就确定下来
        //s5直接引用常量池中的"lancibe"
        String s5 = "l"+"an"+"cibe";
        //s6后面的字符串值不能在编译时就确定下来
        //不能引用常量池中的字符串
        String s6 = s2+s3;
        //使用new调用构造器将会创建一个新的String对象
        //s7应用堆内存中新创建的String对象
        String s7 = new String("lancibe");
        System.out.println(s1==s4);//true
        System.out.println(s1==s5);//true
        System.out.println(s1==s6);//false
        System.out.println(s1==s7);//false

    }
}
  • JVM常量池保证相同的字符串直接量只有一个,不会产生多个副本。
  • 使用new String();创建的字符串对象是运行时创建出来的,他被保存在运行时内存区内,不会放入常量池中。
  • 但在很多时候,程序判断两个引用变量是否相等时,也希望有一种类似于“值相等”的判断,并不严格要求两个引用变量指向同一个对象。此时就可以利用String对象的equals()方法,如上面的程序中的str1.equals(str2);将返回true。
  • 该方法是Object类提供的一个实例方法,因此所有的引用变量都可以调用该方法来判断是否与其他引用变量相等。但使用这个方法判断两个对象相等的标准与使用==运算符没有区别,同样要求引用变量指向同一个对象才返回true。因此这个Object类提供的equals方法没有太大的实际意义,如果希望采用自定义的相等标准,则可采用重写该方法来实现。
package OverrideEqualsError;
class Person
{
    public boolean equals(Object obj)
    {
        return true;
    }
}
class Dog{}
public class OverrideEqualsError {
    public static void main(String[] args)
    {
        Person p = new Person();
        System.out.println("Person对象是否equals Dog对象?"
                +p.equals(new Dog()));
        System.out.println("Person对象是否equals String对象?"
                +p.equals(new String("hello")));
    }
}
  • 造成程序的荒诞结果的原因是由于重写Person类的equals()方法时没有任何判断,无条件的返回true。
package OverrideEqualsRight;
class Person
{
    private String name;
    private String idStr;
    public Person(){}
    public Person(String name, String idStr)
    {
        this.name = name;
        this.idStr = idStr;
    }

    //Java推荐使用的getter和setter方法
    public void setName(String name) {
        this.name = name;
    }
    public void setIdStr(String idStr) {
        this.idStr = idStr;
    }

    public String getName() {
        return name;
    }
    public String getIdStr() {
        return idStr;
    }

    public boolean equals(Object obj)
    {
        //如果两个对象是同一个对象
        if(this == obj)
            return true;

        //只有当obj是Person对象
        if(obj != null && obj.getClass() == Person.class)
        {
            Person personObj = (Person)obj;
            //并且当前对象的idStr与obj对象的idStr相等时才可以判断两个对象是否相等
            if(this.getIdStr().equals(personObj.getIdStr()))
            {
                return true;
            }
        }
        return false;
    }

}
public class OverrideEqualsRight {
    public static void main(String[] args)
    {
        Person p1 = new Person("lancibe", "19");
        Person p2 = new Person("babylan", "19");
        Person p3 = new Person("lancibe", "20");

        System.out.println("p1和p2是否相等?"
                + p1.equals(p2));
        System.out.println("p2和p3是否相等?"
                + p2.equals(p3));
    }
}
  • 通常而言,正确地重写equals()方法应该满足下列条件。
    • 自反性:对任意x,x.equals(x)一定返回true
    • 对称性:对任意x和y,如果y.equals(x)返回true,则x.equals(y)也应该返回true
    • 一致性:对任意x和y,如果判断信息没有改变,那么无论调用多少次x.equals(y)返回的结果都应该保持一致。
    • 对任何不是null的x,x.equals(null)一定返回false。
  • Object默认提供的equals()只是比较对象的地址,即与==比较的结果完全相同。因此在实际应用中常常需要重写equals方法。

final修饰符

  • final关键字可用于修饰类、变量和方法,final关键字有点类似于C#里的sealed关键字,用于表示他修饰的类、方法和变量不可改变。
  • final修饰变量时,表示该变量一旦获得了初始值就不可被改变,final既可以修饰成员变量(包括类变量和实例变量),也可以修饰局部变量、形参。
final成员变量
  • 对于final修饰的成员变量而言,一旦有了初始值,就不能被重新赋值,如果既没有在定义成员变量时指定初始值,也没有在初始化块、构造器中为成员变量指定初始值,那么这些成员变量的值将一直是系统默认分配的0、'\u0000'、false或null,这些成员变量也就完全失去了存在的意义。因此final修饰的成员变量必须由程序员显式的指定初始值
    • 类变量:必须在静态初始化块中指定初始值或声明该类变量时指定初始值,而且只能在两个地方的其中之一指定。
    • 实例变量:必须在非静态初始化块、声明该实例变量或构造器中指定初始值,而且只能在三个地方中的其中之一来指定。
public class FinalVariableTest {
    //定义成员变量时指定默认值,合法
    final int a = 6;

    //下面变量将在构造器或初始化块中分配初始值
    final String str;
    final int c;
    final static double d;
    //初始化块,可以对没有指定默认值的实例变量指定初始值
    {
        str = "hello";
        //不能为a重新赋值,因此下面语句非法
        //a = 9;
    }
    //静态初始化块,可以对没有指定默认值的类变量指定初始值
    static {
        d = 5.6;
    }

    //构造器,可对既没有指定初始值,又没有在初始化块中指定初始值的实例变量指定初始值
    public FinalVariableTest()
    {
        //如果在初始化块中已经对str指定了初始值
        //那么在构造器中不能对final变量赋初始值,下面赋值语句非法
        // str = 'java';
        c = 5;
    }
    public void changeFinal()
    {
        //普通方法不能为final修饰的成员变量赋值
        //d = 1.2;
        //不能再普通方法中为final成员变量指定初始值
        //ch = 'a';
    }
    public static void main(String[] args)
    {
        FinalVariableTest ft = new FinalVariableTest();
        System.out.println(ft.a);
        System.out.println(ft.c);
        System.out.println(ft.d);
    }
}
  • 如果打算在构造器、初始化块中对final成员变量进行初始化,则不要在初始化之前访问成员变量;但Java又允许通过方法来访问final成员变量,此时会看到系统讲final成员变量赋值为0或类似功能的数值。
public class FinalErrorTest {
    //定义一个final修饰的实例变量
    //系统不会对final成员变量进行默认初始化
    final int age;

    {
        //age没有初始化,所以此处代码将会引起错误
        //System.out.println(age);
        printAge();
        age = 6;
        System.out.println(age);
    }

    public void printAge(){
        System.out.println(age);
    }
    public static void main(String[] args)
    {
        new FinalErrorTest();
    }
}
  • 会发现上面程序输出了0、6。
final局部变量
  • 系统不会对局部变量进行初始化,局部变量必须由程序员显式初始化。因此使用final修饰局部变量时,即可以在定义时指定默认值,也可以不指定默认值。
public class LocalVariableTest {
    public void test(final int a)
    {
        //不能对final修饰的形参赋值
        //a = 5;
    }

    public static void main(String[] args)
    {
        //定义final局部变量时指定默认值,则str变量无法被赋值
        final String str = "hello";
        //下面语句非法
        //str = "lancibe";
        final double d;
        //第一次赋值,允许
        d = 5.6;
        //重复赋值,非法
        //d = 3.14;

    }
}
final修饰基本类型变量和应用类型变量的区别
  • 当使用final修饰基本类型变量时,不能对基本类型变量重新进行赋值,因此基本类型变量不能被改变。
  • 但对于引用类型变量来说,他保存的仅仅是一个引用,final只保证这个引用类型变量所引用的地址不会改变,即一直应用同一个对象,当这个对象完全可以发生改变。
package finalreferencetest;

import java.util.Arrays;

class Person
{
    private int age;
    public Person(){}

    public Person(int age)
    {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
public class FinalReferenceTest {
    public static void main(String[] args)
    {
        //final修饰数组变量,iArr是一个引用变量
        final int[] iArr = {5,6,12,9};
        System.out.println(Arrays.toString(iArr));
        //对数组元素进行排序,合法
        Arrays.sort(iArr);
        System.out.println(Arrays.toString(iArr));

        //对数组元素赋值,合法
        iArr[2] = -9;
        System.out.println(Arrays.toString(iArr));

        //对iArr重新赋值,非法
        //iArr = null;
        //final修饰Person变量,p是一个引用变量
        final Person p = new Person(45);
        //改变Person对象的age实例变量,合法
        p.setAge(32);
        System.out.println(p.getAge());
        //下面语句对p重新赋值,非法
        //p = null;
    }
}
  • 除此之外,Java修饰的方法不可被重写,Java修饰的类不可被作为父类
不可变类
  • 不可变类(immutable)的意思是创建该类的实例后,该实例的实例变量是不可改变的。Java提供的8个包装类和java.lang.String类都是不可变类,当创建他们的实例后,其实例的实例变量不可变。
  • 其实可以理解为:类不提供相应的方法来修改这个实例变量的值。
  • 如果需要创建自定义的不可变类,可遵循以下规则:
    • 使用private和final修饰符来修饰该类的成员变量。
    • 提供带参数的构造器,用于根据传入参数来初始化类里的成员变量。
    • 仅为该类的成员变量提供getter()方法,不要提供setter()方法,因为普通方法无法修改final修饰的成员变量。
    • 如果有必要,重写Object类里的hashCode()equals()方法。
public class Address {
    private final String detail;
    private final String postCode;

    public Address()
    {
        this.detail = "";
        this.postCode = "";
    }
    public Address(String detail, String postCode)
    {
        this.detail = detail;
        this.postCode = postCode;
    }

    //仅为两个实例变量提供getter方法
    public String getDetail() {
        return detail;
    }
    public String getPostCode() {
        return postCode;
    }

    //重写equals()方法,判断两个对象是否相等
    public boolean equals(Object obj)
    {
        if(this == obj)
            return true;
        if(obj != null && obj.getClass() == Address.class)
        {
            Address ad = (Address)obj;
            if(this.getDetail().equals(ad.getDetail()) && this.getPostCode().equals(ad.getPostCode()))
                return true;
        }
        return false;
    }
    public int hashCode()
    {
        return detail.hashCode() + postCode.hashCode() * 31;
    }

}

如果需要设计一个不可变类,尤其要注意其引用类型的成员变量,如果引用类型的成员变量的类是可变的,就必须采取必要的措施来保护该成员变量所引用的对象不会被修改,这样才能创建真正的不可变类。

抽象类

  • 当编写一个类时,常常会为该类定义一些方法,这些方法用以描述该类的行为方式,那么这些方法都有具体的方法体。但是在某些情况下,某个父类只是知道其子类应该包含怎样的方法。
抽象方法和抽象类
  • 抽象方法和抽象类必须使用abstract修饰符来定义,有抽象方法的类只能被定义为抽象类,抽象类里可以没有抽象方法。
    • 抽象类必须使用abstract修饰符来修饰,抽象方法也必须使用它来修饰,抽象方法不能有方法体
    • 抽象类不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例。即使抽象类里不包含抽象方法,这个抽象类也不能创建实例
    • 抽象类可以包含成员变量、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类(接口,枚举)5种成分。抽象类的构造器不能用于创建实例,主要是用于被其子类调用。
    • 含有抽象方法的类(包括直接定义了一个抽象方法;或继承了一个抽象父类,但没有完全实现父类包含的抽象方法;或实现了一个接口,但没有完全实现接口包含的抽象方法;或实现了一个接口,但没有完全实现接口包含的抽象方法三种情况)只能被定义成抽象类。
  • 定义抽象方法只需要在普通方法上面加上abstract修饰符,并把普通方法的方法体(也就是方法后花括号括起来的部分)全部去掉,并在方法后增加分号即可。
public abstract class Shape {
    {
        System.out.println("执行初始化块");
    }
    private String color;
    //定义一个计算周长的抽象方法
    public abstract double calPerimeter();
    //定义一个返回形状的抽象方法
    public abstract String getType();

    //定义Shape的构造器,该构造器并不是用于创建Shape对象
    //而是用于被子类调用
    public Shape(){}
    public Shape(String color)
    {
        System.out.println("执行Shape的构造器");
        this.color=color;
    }

    public String getColor() {
        return color;
    }
    public void setColor(String color) {
        this.color = color;
    }
}
  • 抽象类不能用于创建实例,只能当做父类被其他子类继承。
  • 下面定义一个三角形类,三角形类被定义成普通类,因此必须实现Shape类里的所有抽象方法。
package shape;

public class Circle extends shape.Shape {
    private double radius;
    public Circle(String color, double radius)
    {
        super(color);
        this.radius=radius;
    }

    public void setRadius(double radius)
    {
        this.radius=radius;
    }

    public double calPerimeter()
    {
        return 2*Math.PI*radius;
    }
    public String getType()
    {
        return getColor() + "圆形";
    }

    public static void main(String[] args)
    {
        shape.Shape s1 = new shape.Triangle("黄色", 3,4,5);
        shape.Shape s2 = new Circle("红色", 3);
        System.out.println(s1.getType());
        System.out.println(s1.calPerimeter());
        System.out.println(s2.getType());
        System.out.println(s2.calPerimeter());
    }
}
  • 执行结果如下:
执行初始化块
执行Shape的构造器
执行初始化块
执行Shape的构造器
三角形
12.0
红色圆形
18.84955592153876
  • 上面的main方法中定义了两个Shape类型的引用变量,他们分别指向Triangle对象和Circle对象,由于在Shape类中定义了calperimeter方法和getType方法,所以程序可以直接调用两个变量的该方法,无法强制类型转换为其子类类型。
  • 利用抽象类和抽象方法的优势,可以更好的发挥多态的优势,使得程序更加灵活。
  • 当使用abstract修饰类时,表明这个类只能被继承;当使用abstract修饰方法时,表明这个方法必须由子类提供实现(即重写)。而final修饰的类不能被继承,故他们两个永远不能一起被使用。

Java9 改进的接口

  • 抽象类是从多个类中抽象出来的模板,如果将这种抽象进行的更彻底,则可以提练出一种更加特殊的“抽象类”:接口。Java9对接口进行了改进,允许在接口中定义默认方法和类方法,默认方法和类方法都可以提供方法实现,Java9为接口增加了一种私有方法,私有方法也可提供方法实现。
Java9中接口的定义
  • 和类定义不同,定义接口不再使用class关键字,而是使用interface关键字,接口定义的基本语法如下:
[修饰符] interface 接口名 extends 父接口1, 父接口2...
{
	零个到多个常量定义
	零个到多个抽象方法定义
	零个到多个内部类、接口、枚举定义
	零个到多个私有方法、默认方法或类方法定义
}
  • 修饰符可以是public或省略,如果省略将采用默认权限访问控制符,即只有在相同包结构下才可以访问该接口。
  • 接口名应与类名采用相同的命名规则,即如果仅从语法角度来看,接口名只要是合法的标识符即可;如果要遵守Java可读性规范,则接口名应由多个有意义的单词连缀而成,每个单词首字母大写,单词与单词之间无需任何分隔符。接口名通常能够使用形容词。
  • 一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。
  • 由于接口定义的是一种规范,因此接口里不能包含构造器和初始化来定义。接口里可以包含成员变量(只能是静态常量)、方法(只能是抽象实例方法、类方法、默认方法或私有方法)、内部类(包括内部借口、枚举)定义。
  • 对于接口里定义的静态常量而言,他们是接口相关的,因此系统会自动为这些成员添加static和final两个修饰符。也就是说,在接口中定义成员变量时,不管是否使用public static final修饰符,接口里的成员变量总是以这三个修饰符来修饰。而且接口里没有构造器和初始化块,因此接口里定义的成员变量只能在定义时指定默认值。
  • 接口里定义的方法只能是抽象方法、类方法、默认方法或私有方法,因此如果不是定义默认方法、类方法或私有方法,系统会自动为普通方法增加abstract修饰符;定义接口里的普通方法时不管是否使用public abstract修饰符,接口里的普通方法总是以它来修饰。接口里的普通方法不能有方法实现(方法体);但类方法、默认方法、私有方法都必须有方法实现。

接口里定义的内部类、内部借口、内部枚举默认都采用public static两个修饰符,不管定义是否指定这两个修饰符,系统都会自动使用它对他们进行修饰。

  • 下面定义一个接口
public interface Output {
    int MAX_CHCHE_LINE = 50;

    //接口中定义的普通方法只能是public的抽象方法
    void out();
    void getData(String msg);

    //在接口中默认方法,需要使用default修饰
    default void print(String... msgs)
    {
        for (String msg:msgs)
        {
            System.out.println(msg);
        }
    }
    default void test()
    {
        System.out.println("默认的抽象方法");
    }

    //在接口中定义类方法,需要使用static修饰
    static String staticTest()
    {
        return "接口里的类方法";
    }

    //定义私有方法
    private void foo()
    {
        System.out.println("foo私有方法");
    }

    //定义私有静态方法
    private static void bar()
    {
        System.out.println("bar私有静态方法");
    }
}
  • 接口的默认方法其实就是实例方法,但由于早期Java的设计是:接口中的实例方法不能有方法体;Java8也不能直接“推倒”以前的规则,只好重定义一个所谓的默认方法,默认方法就是方法体的实例方法。
  • Java8允许在接口中定义类方法,类方法必须使用static修饰,该方法不能使用default修饰,无论程序是否指定,类方法总是使用public修饰——如果开发者没有指定public,系统会为其自动添加。类方法可以直接使用接口来调用。
  • Java9增加了带方法体的私有方法,这也是Java8埋下的伏笔:Java8允许在接口中定义带方法体的默认方法和类方法——这样势必会引发一个问题,当两个默认方法(或类方法)中包含一段相同的实现逻辑时,程序必然考虑将这段实现逻辑抽取成工具方法,而工具方法是应该被隐藏的,这就是Java9增加私有方法的必然性。
  • 接口里的成员变量默认是使用public static final修饰的,因此即使另一个类处于不同包下,也可以通过接口来访问接口里的成员变量。
public class OutputFieldTest {
    public static void main(String[] args)
    {
        System.out.println(Output.MAX_CHCHE_LINE);
        System.out.println(Output.staticTest());
    }
}
  • 从上面main方法中可以看出,即使该类与output处于不同包下,仍可以访问他的成员变量,这说明他是public访问权限,而且可以通过借口来访问该成员变量,表明该成员变量是一个类变量;当为这个成员变量赋值时会引发“为final变量赋值”的编译异常,表明这个成员变量使用了final修饰。

从某种角度来看,接口可以被当成一种特殊的类,因此一个Java源文件里最多只有一个public接口,如果一个Java源文件里定义了一个public接口,则该源文件的主文件名必须与该接口名相同。

接口的继承
  • 接口的继承和类的继承不一样,接口完全支持多继承,即一个接口可以有多个直接父接口。和类继承相似,子接口扩展某个父接口,将会获得父接口里定义的所有抽象方法、常量。
  • 一个接口继承多个父接口时,多个父接口排在extends关键字之后,多个父接口之间以英文逗号隔开。下面程序定义了三个接口,第三个接口继承了前面两个接口。
interface InterfaceA {
    int PROP_A = 5;
    void testA();
}

interface InterfaceB{
    int PROP_B = 6;
    void testB();
}

interface InterfaceC extends InterfaceA, InterfaceB
{
    int PROP_C = 7;
    void testC();
}

public class InterfaceExtendsTest
{
    public static void main(String[] args)
    {
        System.out.println(InterfaceC.PROP_A);
        System.out.println(InterfaceC.PROP_B);
        System.out.println(InterfaceC.PROP_C);
    }
}
使用接口
  • 接口不能用于创建实例,但接口可以用于声明引用类型变量。当使用接口来声明引用类型变量时,这个引用类型变量必须引用到其实现类的对象。除此之外,接口的主要用途是被实现类实现。归纳起来,接口主要有如下用途。
    1. 定义变量,也可以用于强制类型转换
    2. 调用接口中定义的常量
    3. 被其他类实现
  • 一个类可以实现一个或多个接口,继承使用extends关键字,实现则使用implements关键字。因为一个类可以实现多个接口,这也是Java为单继承灵活性不足所做的补充。类实现接口的语法格式如下:
[修饰符] class 类名 extends 父类 implements 接口1,接口2...
{
	类体部分
}
  • 一个类实现了一个或多个接口之后,这个类必须完全实现这些接口里所定义的全部抽方法(也就是重写这些抽象方法);否则,该类讲保留从父接口那里继承到的抽象方法,该类也必须定义成抽象类。
  • 一个类实现某个接口时,该类将会获得接口中定义的常量(成员变量)、方法等,因此可以把实现接口理解成一种特殊的继承,相当于实现类继承了一个彻底抽象的类(相当于除默认方法外,所有方法都是抽象方法的类)。
//定义一个Product接口
interface Product
{
    int getProduceTime();
}
//让printer类实现Output和Product接口
public class Printer implements Output, Product {
    private String[] printData = new String[Output.MAX_CHCHE_LINE];
    //用以记录当前需打印的作业数
    private int dataNum = 0;
    public void out()
    {
        //只要还有作业,就继续打印
        while(dataNum > 0)
        {
            System.out.println("打印机打印:"+printData[0]);
            //把作业队列整体前移一位,并将剩下的作业数减一
            System.arraycopy(printData, 1, printData, 0, --dataNum);
        }
    }
    public void getData(String msg)
    {
        if(dataNum >= Output.MAX_CHCHE_LINE)
        {
            System.out.println("输出队列已满,添加失败");
        }
        else
        {
            //把打印数据添加到队列里,已保存数据的数量加一
            printData[dataNum++] = msg;
        }
    }

    @Override
    public int getProduceTime() {
        return 45;
    }
    public static void main(String[] args)
    {
        //创建一个Printer对象,当成Output使用
        Output o = new Printer();
        o.getData("lancibe");
        o.getData("babyxun");
        o.out();
        o.getData("Fei");
        o.getData("babyfei");
        o.out();
        //调用Output接口中定义的默认方法
        o.print("first", "second", "third");
        o.test();
        //创建一个Printer对象,当成Product使用
        Product p = new Printer();
        System.out.println(p.getProduceTime());

        //所有接口类型的引用变量都可以直接赋给Object类型的变量
        Object obj = p;
    }
}
  • 从上面程序中可以看出,Printer类实现Output接口和Product接口,因此Printer对象即可以直接赋给Output变量,也可以直接赋给Product变量。仿佛Printer类既是Output的子类,也是Product的子类,这就是Java提供的模拟多继承。
  • 上面程序中Printer实现了Output接口,即可获取Output接口中定义的print()和test()两个默认方法,因此Print实例可以直接调用这两个默认方法。
  • 接口不能显式继承任何类,但所有接口类型的引用变量都可以直接赋给Object类型的引用变量。所以在上面程序中可以把Product类型的变量直接赋给Object类型变量,这是利用向上转型来实现的,因为编译器知道任何Java对象都必须是Object或其子类的实例,Product类型的对象也不例外(它必须是Product接口实现类的对象,该实现类肯定是Object的显式或隐式子类)。
接口和抽象类
  • 接口和抽象类很像,他们都具有如下特征。
    • 接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其它类实现和继承。
    • 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。
  • 但接口和抽象类之间的差别非常大,主要体现在二者设计目的上。
  • 接口作为系统与外界交互的窗口,接口体现的是一种规范。对于接口的实现者而言,接口规定实现者必须向外提供哪些服务(以方法的形式来提供);对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务(就是如何来调用方法)。但在一个程序中使用接口时,接口是多个模块间的耦合标准;当在多个应用程序之间使用接口时,接口是多个程序之间的通信标准。
  • 从某种程度上来看,接口类似于整个系统的“总纲”,他制订了系统各模块应该遵循的标准,因此一个系统中的接口不应该经常改变。一旦接口被改变,对整个系统甚至其他系统的影响将是辐射式的,导致系统中大部分类都需要改写。
  • 抽象类则不一样,抽象类作为系统中多个子类的共同父类,它所体现的是一种模板式设计。抽象类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能(那些已经提供实现的方法),但这个产品依然不能当成最终产品,必须有更进一步的完善,这种完善可能有几种不同方式。
  • 除此之外,接口和抽象类在用发上也存在如下差别。
    • 接口里只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现,抽象类则完全可以包含普通方法。
    • 接口里只能定义静态常量,不能定义普通成员变量;抽象类里都可以
    • 接口里不包含构造器;抽象类里可以包含构造器,但并不是用于创建对象,而是让子类调用这些构造器来完成属于抽象类的初始化操作。
    • 接口里不能包含初始化块;但抽象类完全可以。
    • 一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口,通过实现多个借口可以弥补Java单继承的不足。
面向接口编程
简单工厂模式
  • 有一个场景:假设程序中有个Computer类需要组合一个输出设备,现在有两个选择:直接让Computer类组合一个Printer,或者让Computer类组合一个Output,那么到底采用哪种方式更好呢?
  • 假设使用第一种方法,如果有一天系统需要重构,需要使用Betterprinter来代替,这就需要打开Computer类的源代码进行修改。如果系统中只有一个Computer类组合了Printer还好,当时如果数量多了,这就是相当大的工作量。
  • 为了避免这一问题,工厂模式建议让Computer类组合一个Output类型的对象,将Computer类与Printer类完全分离。Computer对象实际上组合的是Printer对象还是Betterprinter对象,对Computer而言完全透明。当Printer对象切换到Betterprinter对象时,系统完全不受影响。下面是这个Computer定义代码。
public class Computer {
    private Output out;
    public Computer(Output out)
    {
        this.out = out;
    }
    //定义一个模拟获取字符串输入的方法
    public void keyIn(String msg)
    {
        out.getData(msg);
    }
    //定义一个模拟打印的方法
    public void print()
    {
        out.out();
    }
}
  • 上面的Computer类已经完全与Printer分离,只是与Output接口耦合。Computer不在负责创建Output对象,系统要提供一个Output工厂来负责生成Output对象。这个Outputfactory工厂类代码如下
public class OutputFactory {
    public Output getOutput()
    {
        return new Printer();
    }
    public static void main(String[] args)
    {
        OutputFactory of = new OutputFactory();
        Computer c = new Computer(of.getOutput());
        c.keyIn("lancibe");
        c.keyIn("fei");
        c.print();
    }
}
  • 在该OutputFactory类中包含了一个getoutput()方法,该方法返回一个Output实现类的实例,该方法负责创建Output实例,具体创建哪一个实现类的对象由该方法决定(具体由该方法中的粗体部分控制,当然也可以增加更复杂的控制逻辑)。如果系统需要将Printer改为BetterPrinter实现类,只需让BetterPrinter实现Output接口,并改变OutputFactory类中的getOutput()方法即可。
  • 下面是BetterPrinter实现类的代码,BetterPrinter只是对原有的Printer进行简单修改,以模拟系统重构后的改进。
public class BetterPrinter implements Output {
    private String[] printData = new String[Output.MAX_CHCHE_LINE*2];
    //用以记录当前需打印的作业数
    private int dataNum = 0;
    public void out()
    {
        //只要还有作业,就继续打印
        while(dataNum > 0)
        {
            System.out.println("高速打印机正在打印:"+printData[0]);
            //把作业队列整体前移一位,并将剩下的作业数减一
            System.arraycopy(printData, 1, printData, 0, --dataNum);
        }
    }
    public void getData(String msg)
    {
        if(dataNum >= Output.MAX_CHCHE_LINE * 2)
        {
            System.out.println("输出队列已满,添加失败");
        }
        else
        {
            //把打印数据添加到队列里,已保存数据的数量加一
            printData[dataNum++] = msg;
        }
    }
}
  • 上面的Betterprinter类也实现了Output接口,因此也可当成Output对象来使用,于是只需要把Outputfactory工厂类的getOutput()方法中的返回值改为return new BetterPrinter();即可。
  • 再次运行前面的OutputFactory.java程序,发现系统运行时已改为Betterprinter对象。
命令模式
  • 考虑这样一种场景:某个方法需要完成一个行为,当这个行为的具体实现无法确定,必须等到执行该方法时才可以确定。具体一点:假设有个方法需要遍历某个数组的数组元素,但无法确定在遍历数组元素时如何处理这些元素,需要在调用该方法时指定具体的处理行为。
  • 这个要求看起来有点奇怪:这个方法不进需要普通数据可以变化,甚至还有方法执行体也需要变化,难道把“处理行为”作为一个参数传入该方法?

在某些编程语言(如Ruby等)里,确实允许传入一个代码快作为参数。现在Java8已经增加了Lambda表达式,通过Lambda表达式可以传入代码块作为参数。

  • 对于这样的一个需求,必须把“处理行为”作为参数传入该方法,这个“处理行为”用编程实现是一段代码。那如何把这段代码传入该方法呢。
  • 可以考虑使用一个Command接口来定义一个方法,以此封装“处理行为”。下面是Command接口的代码。
public interface Command {
    //接口里定义的process方法用于封装“处理行为”
    void process(int[] target);
}
  • 上面的Command接口里定义了一个process()方法,该方法用于封装“处理行为”,但这个方法没有方法体——因为现在还无法确定这个处理行为。
  • 下面是需要处理数组的处理类,在这个处理类中包含一个process()方法,这个方法无法确定处理数组的处理行为,所以定义时该方法使用了一个Command参数,这个Command参数负责对数组的处理行为。该类的程序代码如下:
public class ProcessArray {
    public void process(int[] target, Command cmd)
    {
        cmd.process(target);
    }
}
  • 通过一个Command接口,就实现了让ProcessArray类和具体“处理行为”的分离,程序使用Command接口代表了对数组的处理行为。Command接口也没有提供真正的处理,只有等到需要调用ProcessArray对象的process()方法时,才真正传入一个Command对象,才确定对数组的处理行为。
  • 下面的程序示范了对数组的两种处理方式。
public class CommandTest {
    public static void main(String[] args)
    {
        ProcessArray pa = new ProcessArray();
        int[] target = {3, -4, 6, 4};
        //第一次处理数据,具体处理行为取决于PrintCommand
        pa.process(target, new PrintCommand());
        System.out.println("---------------");
        pa.process(target, new AddCommand());
    }
}
public class PrintCommand implements Command {
    public void process(int[] target)
    {
        for (int tmp: target)
        {
            System.out.println("迭代输出目标数组元素:"+tmp);
        }
    }
}
public class AddCommand implements Command {
    public void process(int[] target)
    {
        int sum = 0;
        for(int tmp:target)
        {
            sum += tmp;
        }
        System.out.println("数组元素总和是:"+sum);
    }
}

内部类

  • 大部分时候,类被定义成一个独立的程序单元。在某些情况下,也会把一个类放在另一个类的内部定义,这个定义在其他类内部的类就被称为内部类(有的地方也叫嵌套类),包含内部类的类也被称为外部类(有的地方也叫宿主类)。Java从JDK 1.1开始引入内部类,内部类主要有如下作用。
    • 内部类提供了更好的封装,可以吧内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。假设需要创建Cow类,Cow类需要组合一个Cowleg对象,Cowleg类只有在Cow类里才有效,离开了Cow类之后没有任何意义。在这种情况下,就可以把Cowleg定义成Cow的内部类,不允许其他类访问Cowleg。
    • 内部类成员可以直接访问外部类的私有数据,因为内部类被当成其外部类成员,同一个类的成员之间可以相互访问。但外部类不能访问内部类的实现细节,例如内部类的成员变量。
    • 匿名内部类适合用于创建那些仅需要一次使用的类。对于前面介绍的命令模式,但需要传入一个Command对象时,重新专门定义Printcommand和Addcommand两个实现类可能没有太大意义,因此这两个实现类可能仅需要使用一次。在这种情况下,使用匿名内部类将更加方便。
  • 从语法角度来看,定义内部类与定义外部类的语法大致相同,内部类除需要定义在其他类里面之外,还存在如下两点区别。
    • 内部类比外部类可以多使用三个修饰符:private、protected、static——外部类不可以使用这三个修饰符
    • 非静态内部类不能拥有静态成员。
非静态内部类
  • 定义内部类非常简单,只要把一个类放在另一个类内部定义即可。此处的“类内部”包括类中的任何位置,甚至在方法中也可以定义内部类(方法里定义的内部类被称为局部内部类)。
  • 下面程序在Cow类里定义了一个Cowleg非静态内部类,并在Cowleg类的实例方法中直接访问Cow的private访问权限的实例变量。
public class Cow {
    private double weight;

    //外部类的两个重载的构造器
    public Cow(){}
    public Cow(double weight)
    {
        this.weight = weight;
    }
    //定义一个非静态内部类
    private class CowLeg
    {
        //非静态内部类的两个实例变量
        private double length;
        private String color;
        //非静态内部类的两个重载的构造器
        public CowLeg(){}
        public CowLeg(double length, String color)
        {
            this.length = length;
            this.color = color;
        }

        public String getColor() {
            return color;
        }

        public double getLength() {
            return length;
        }

        public void setColor(String color) {
            this.color = color;
        }

        public void setLength(double length) {
            this.length = length;
        }
        //非静态内部类的实例方法
        public void info()
        {
            System.out.println("当前牛腿的颜色是:"+color+",高:"+length);
            //直接访问外部类的private修饰的成员变量
            System.out.println("本牛腿所在的奶牛重:"+weight);
        }
    }
    public void test()
    {
        CowLeg cl = new CowLeg(1.12, "黑白相间");
        cl.info();
    }
    public static void main(String[] args)
    {
        Cow cow = new Cow(378.9);
        cow.test();
    }
}
  • 编译上面程序,看到文件中生成了两个class文件,一个是Cow.class,一个是Cow C o w L e g . c l a s s 。 可 见 , 内 部 类 的 c l a s s 文 件 总 是 以 这 种 形 式 : ‘ O u t e r C l a s s CowLeg.class。可见,内部类的class文件总是以这种形式:`OuterClass CowLeg.classclassOuterClassInnerClass.class`
  • 当在非静态内部类的方法内访问某个变量时,系统会优先查找该方法内是否有局部变量,再查找内部类是否存在,再查找外部类,如果依然不存在,则编译错误:找不到该变量。
  • 因此,如果外部类成员变量、内部类成员变量与内部类里方法的局部变量同名,则可以通过this、外部类类名.this作为限定来区分。
public class DiscernVariable {
    private String prop = "外部类的实例变量";
    private class InClass
    {
        private String prop = "内部类的实例变量";
        public void info()
        {
            String prop = "局部变量";
            //通过外部类类名.this.varName访问外部类实例变量
            System.out.println(DiscernVariable.this.prop);
            //通过this.varName访问内部类实例的变量
            System.out.println(this.prop);
            //直接访问局部变量
            System.out.println(prop);
        }
    }
    public void test()
    {
        InClass in = new InClass();
        in.info();
    }
    public static void main(String[] args)
    {
        new DiscernVariable().test();
    }
}
  • 上面程序中粗体字代码行分别访问外部类的实例变量、非静态内部类的实例变量。通过OutterClass.this.propName的形式访问外部类的实例变量,通过this.propName的形式访问非静态内部类的实例变量。
  • 非静态内部类的成员可以访问外部类的private成员,但反过来就不成立了。非静态内部类的成员只在非静态内部类范围内是可知的,并不能被外部类直接使用。如果外部类需要访问非静态内部类的成员,则必须显式创建非静态内部类对象来调用访问其实例成员。
public class Outer {
    private int outProp = 9;
    class Inner
    {
        private int inProp = 5;
        public void accessOuterProp()
        {
            //非静态内部类可以直接访问外部类的private成员变量
            System.out.println("外部类的outProp值:"+outProp);
        }
    }
    public void accessInnerProp()
    {
        //外部类不能直接访问非静态内部类的实例变量
        //下面代码编译错误
        //System.out.println("内部类的inProp值:"+inProp);
        //如需访问内部类的实例变量,必须显式创建内部类对象
        System.out.println("内部类的inProp值:"+new Inner().inProp);
    }
    public static void main(String[] args)
    {
        //执行下面代码,只创建了外部类对象,还未创建内部类对象
        Outer out = new Outer();
        out.accessInnerProp();
    }
}
  • 根据静态成员不能访问非静态成员的规则,外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例等。总之,不允许在外部类的静态成员中直接使用非静态内部类。
public class StaticTest {
    //定义一个非静态的内部类,是一个空类
    private class In{}
    //外部类的静态方法
    public static void main(String[] args)
    {
        //下面代码引起编译异常,因为静态成员(main()方法)无法访问非静态成员(In类)
        //new In();
    }
}
  • Java不允许在非静态内部类里定义静态成员。下面程序示范了非静态内部类包含静态成员将引发编译错误
public class InnerNoStatic {
    private class InnerClass
    {
        static
        {
            System.out.println("=====");
        }
        private static int inProp;
        private static void test(){};
    }
}
  • 非静态内部类里不能有静态方法、静态成员变量、静态初始化块。所以上面三个静态声明都会引发错误。
静态内部类
  • 如果使用static来修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的莫个对象。因此使用static修饰的内部类称为类内部类,也称为静态内部类。
  • 静态内部类可以包含静态成员,也可以包含非静态成员。根据静态成员不能访问非静态成员的规则,静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。即使静态内部类的实例方法,也不能访问外部类的实例成员,只能访问外部类的静态成员。
public class StaticInnerClassTest {
    private int prop1 = 5;
    private static int prop2 = 9;
    static class StaticInnerClass
    {
        //静态内部类里可以包含静态成员
        private static int age;
        public void accessOuterProp()
        {
            //如果访问prop1,则出现错误
            System.out.println(prop2);
        }
    }
}
  • 外部类依然不能直接访问静态内部类的成员,但是可以使用静态内部类的类名作为调用者来访问静态内部类的类成员,也可以使用静态内部类对象作为调用者来访问静态内部类的实例成员。
public class AccessStaticInnerClass {
    static class StaticInnerClass
    {
        private static int prop1 = 5;
        private int prop2 = 9;
    }
    public void accessInnerProp()
    {
        //类名.变量名
        System.out.println(StaticInnerClass.prop1);
        //调用构造器并访问
        System.out.println(new StaticInnerClass().prop2);
    }
}
  • 除此之外,Java还允许在接口里定义内部类,接口里定义的内部类默认使用public static修饰,也就是说,接口内部类只能是静态内部类。
局部内部类
  • 如果把一个内部类放在方法里定义,则这个内部类就是一个局部内部类,局部内部类仅在该方法内有效。由于局部内部类不能再外部类的方法意外的地方使用,因此局部内部类也不能使用访问控制符static修饰符来修饰。
public class LocalInnerClass {
    public static void main(String[] args)
    {
        //定义局部内部类
        class InnerBase
        {
            int a;
        }
        class InnerSub extends InnerBase
        {
            int b;
        }
        //创建局部内部类的对象
        InnerSub is = new InnerSub();
        is.a = 5;
        is.b = 9;
        System.out.println(""+(is.a+is.b));
    }
}
匿名内部类
  • 匿名内部类适合创建那种只需要一次使用的类,例如前面介绍命令模式时需要的Command对象。匿名内部类的语法有点奇怪,创建匿名内部类时会立即创建一个该类的实例,这个类定义会立即消失,匿名内部类不能重复使用。
  • 创建匿名内部类的语法格式如下
new 实现接口() | 父类构造器(实参列表)
{
	//匿名内部类的类体部分
}
  • 最常用的创建匿名内部类的方式是需要创建某个接口类型的对象,如下所示
interface Product
{
    public double getPrice();
    public String getName();
}
public class AnonymousTest {
    public void test(Product p)
    {
        System.out.println("购买了一个"+p.getName()+",花掉了"+p.getPrice());
    }
    public static void main(String[] args)
    {
        AnonymousTest ta = new AnonymousTest();
        ta.test(new Product() {
            @Override
            public String getName()
            {
                return "lancibe";
            }
            public double getPrice()
            {
                return 567.8;
            }
        });
    }
}
  • 当通过实现接口来创建匿名内部类时,匿名内部类也不能显式创建构造器,因此匿名内部类只有一个隐式的无参数构造器,故new接口名后的括号里不能传入参数值。但如果通过继承父类来创建匿名内部类时,匿名内部将拥有和父类相似的构造器,此处的相似指的是拥有相同的形参列表。
abstract class Device
{
    private String name;
    public abstract double getPrice();
    public Device(){};
    public Device(String name)
    {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
public class AnonymousInner {
    public void test(Device d)
    {
        System.out.println("购买了一个"+d.getName()+",花掉了"+d.getPrice());
    }
    public static void main(String[] args)
    {
        AnonymousInner ai = new AnonymousInner();
        ai.test(new Device("电子示波器") {
            @Override
            public double getPrice() {
                return 67.8;
            }
        });
        //调用无参数构造器创建Device匿名实现类的对象
        Device d = new Device() {
            //初始化块
            {
                System.out.println("匿名内部类的初始化块");
            }
            @Override
            public double getPrice() {
                return 56.2;
            }
            //重写父类的实例方法
            public String getName()
            {
                return "键盘";
            }
        };
        ai.test(d);
    }
}

Java8 新增的Lambda表达式

  • Lambda表达式是Java8的重要更新,他允许使用更简洁的代码来创建只有一个抽象方法的接口(函数式接口)的实例
Lambda表达式入门
  • 使用匿名内部类来改写前面介绍的Command表达式的例子,改写后的程序如下:
public class CommandTest {
    public static void main(String[] args)
    {
        ProcessArray pa = new ProcessArray();
        int[] target = {3, -4, 6, 4};
        //处理数组,具体处理行为取决于匿名内部类
        pa.process(target, new Command() {
            public void process(int[] target)
            {
                int sum = 0;
                for(int tmp:target)
                {
                    sum += tmp;
                }
                System.out.println("数组元素的总和是:"+sum);
            }
        });
    }
}
  • Lambda表达式完全可用于创建匿名内部类对象,因此可将上面代码改为如下形式
public class CommandTest2 {
    public static void main(String[] args)
    {
        ProcessArray pa = new ProcessArray();
        int[] array = {3,-4,6,4};
        //处理数组,具体处理行为取决于匿名内部类
        pa.process(array, (int[] target)->{
            int sum = 0;
            for(int tmp:target)
            {
                sum += tmp;
            }
            System.out.println("数组元素的总和是:"+sum);
        });
    }
}
  • 从上面程序中的粗体字代码可以看出,这段代码与创建匿名内部类时所需要实现的process(int[] target)方法完全相同,只是不需要new Xxx(){}这样繁琐的代码,不需要指出重写的方法名字,也不需要给出重写的方法的返回值类型——只要给出重写的方法括号以及括号内的形参列表即可。
  • 从上面介绍可以看出,当使用Lambda表达式代替匿名内部类创建对象时,Lambda表达式的代码块将代替实现抽象方法的方法体,Lambda表达式就相当于一个匿名方法。他有三部分组成:
    1. 形参列表。形参列表允许省略形参类型。如果形参列表中只有一个参数,甚至连形参列表的圆括号也可以省略。
    2. 剪头->
    3. 代码块。如果代码块只包含一条语句,Lambda表达式允许省略代码块的花括号。Lambda表达式需要返回值,而他的代码块仅有一条省略的return语句,Lambda表达式会自动返回这条语句的值。
  • 下面程序示范了Lambda表达式的几种简化写法
interface Eatable
{
    void taste();
}
interface Flyable
{
    void fly(String weather);
}
interface Addable
{
    int add(int a, int b);
}

public class LambdaQs {
    //调用该方法需要Eatable对象
    public void eat(Eatable e)
    {
        System.out.println(e);
        e.taste();
    }
    //调用该方法需要Flyable对象
    public void drive(Flyable f)
    {
        System.out.println("我正在驾驶:"+f);
        f.fly("晴天");
    }
    //调用该方法需要Addable
    public void test(Addable add)
    {
        System.out.println("5和3的和是"+add.add(5,3));
    }
    public static void main(String[] args)
    {
        LambdaQs lq = new LambdaQs();
        lq.eat(()->System.out.println("苹果味道不错"));
        lq.drive(weather->
        {
            System.out.println("今天天气是:"+weather);
            System.out.println("直升机飞行平稳");
        });
        lq.test((a,b)->a+b);
    }
}
  • 上面程序可以正常编译运行,说明lambda表达式实际上将会被当成一个任意类型的对象,到底需要当成各种类型的对象,取决于运行环境的需要。
Lambda表达式与函数式接口
  • Lambda表达式的目标类型必须是“函数式接口”。函数式接口只包含一个抽象方法的接口。函数式接口可以包含多个默认方法、类方法,但只能声明一个抽象方法。
  • 如果采用匿名内部类语法来创建函数式接口的实例,则只需要实现一个抽象方法,在这种情况下即可采用Lambda表达式来创建对象,该表达式创建出来的对象的目标类型就是这个函数式接口。查阅Java8的API文档,可以发现大量的函数式接口,例如:Runnable、ActionListener等接口都是函数式接口。

Java8专门为函数式接口提供了@FunctionalInterface注解,需放在接口定义的前面,它用于告诉编译器执行更严格检查——检查该接口必须是函数式接口,否则编译器会报错。

  • Lambda表达式的结果是被当成对象,因此程序中可以使用Lambda表达式来进行赋值:
Runnable r = ()->{
for (int i = 0 ; i < 100 ; i++)
{
    System.out.println();
}
};
  • Lambda表达式的目标类型必须是明确的函数式接口;Lambda表达式只能为函数式接口创建对象。Lambda表达式只能实现一个方法,因此他只能为只有一个抽象方法的接口(函数式接口)创建对象。
  • 为保证Lambda表达式的目标类型是一个明确的函数式接口,可以有如下三种常见方式:
    1. 将Lambda表达式赋值给函数式接口类型的变量
    2. 将Lambda表达式作为函数式接口类型的参数传给某个方法
    3. 使用函数式接口对Lambda表达式进行强制类型转换
Object obj1 = (Runnable)()->{
for (int i = 0 ; i < 100 ; i++)
{
    System.out.println();
}
};
  • 函数式接口的定义如下:
@FunctionalInterface
interface FkTest
{
	void run();
}
  • 上面的函数式接口中仅定义了一个不带参数的方法,因此前面强制类型转换的Runnable也可以换成FkTest类型。
Object obj2 = (FkTest)() ->{
	for(int i = 0 ; i < 100 ; i++)
	{
	    System.out.println();
	}
};
方法引用与构造器引用
  • Lambda表达式支持的方法引用和构造器引用
种类示例说明对应的Lambda表达式
引用类方法类名::类方法函数式接口中被实现方法的全部参数传给该类方法作为参数(a,b,…)->类名.类方法(a,b,…)
引用特定对象的实例方法特定对象::实例方法函数式接口中被实现方法的全部参数传给该方法作为参数(a,b,…)->特定对象.实例方法(a,b,…)
引用某类对象的实例方法类名::实例方法函数式接口中被实现方法的第一个参数作为调用者,后面的参数全部传给该方法作为参数(a,b,…)->a.实例方法(b,…)
引用构造器类名::new函数式接口中被实现方法的的全部参数传给该构造器作为参数(a,b,…)->new 类名(a,b,…)
  • 对上面四种情况分别举例:
import javax.swing.*;

@FunctionalInterface
interface Converter
{
    Integer convert(String from);
}


@FunctionalInterface
interface MyTest
{
    String test(String a, int b, int c);
}

@FunctionalInterface
interface YourTest
{
    JFrame win(String title);
}


public class MethodRefer {
    public static void main(String[] args)
    {
        //引用类方法
        //Converter converter1 = from-> Integer.valueOf(from);
        Converter converter1 = Integer::valueOf;//两种写法都可
        Integer val = converter1.convert("99");
        System.out.println(val);

        //引用特定对象的实例方法
        //Converter converter2 = from -> "lancibe.org".indexOf(from);
        Converter converter2 = "lancibe.org"::indexOf;
        Integer value = converter2.convert("lan");
        System.out.println(value);

        //引用某类对象的实例方法
        //MyTest mt = (a,b,c) -> a.substring(b,c);
        MyTest mt = String::substring;
        String str = mt.test("I love Java", 2, 9);
        System.out.println(str);

        //引用构造器
        //YourTest yt = (String a) -> new JFrame(a);
        YourTest yt = JFrame::new;
        JFrame jf = yt.win("my window");
        System.out.println(jf);
    }
}
Lambda表达式与匿名内部类的区别和联系
  • 相同点:
    1. Lambda表达式与匿名内部类一样都可以直接访问“effectively final”的局部变量,以及外部类的成员变量(包括实例变量和类变量)。
    2. Lambda表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法。
@FunctionalInterface
interface Displayable
{
    void display();
    default int add (int a , int b)
    {
        return a+b;
    }
}
public class LambdaAndInner {
    private int age = 12;
    private static String name = "1909";
    public void test()
    {
        String book = "Lancibe";
        Displayable dis = ()->
        {
            //访问"effectively final"的局部变量
            System.out.println("book的局部变量为:"+book);
            System.out.println("外部类的age实例变量为:"+age);
            System.out.println("外部类的name类变量为:"+name);
        };
        dis.display();
        System.out.println(dis.add(3, 5));
    }
    public static void main(String[] args)
    {
        LambdaAndInner lambda = new LambdaAndInner();
        lambda.test();
    }
}
使用Lambda表达式调用Arrays的类方法
  • 前面介绍Array类的功能时已经提到,Arrays类的有些方法需要Comparator,XxxOperator,XxxFunction等接口的实例,这些接口都是函数式接口,因此可以使用Lambda表达式来调用Arrays的方法:
import java.util.Arrays;

public class LambdaArrays {
    public static void main(String[] args)
    {
        String[] arr1 = new String[]{"java","lancibe","fkit", "ios", "android"};
        Arrays.parallelSort(arr1, (o1, o2)->o1.length()-o2.length());
        System.out.println(Arrays.toString(arr1));
        int[] arr2 = new int[] {3,-4,25,16,30,18};
        Arrays.parallelPrefix(arr2, (left, right)->left*right);
        System.out.println(Arrays.toString(arr2));
        long[] arr3 = new long[5];
        Arrays.parallelSetAll(arr3, operand -> operand*5);
        System.out.println(Arrays.toString(arr3));
    }
}
  • 其结果如下:
[ios, java, fkit, lancibe, android]
[3, -12, -300, -4800, -144000, -2592000]
[0, 5, 10, 15, 20]
  • Lambda表达式的作用在:第一段指定了判断字符串大小的方式,第二段根据前后两个元素计算当前元素的值,第三段会根据元素的索引来计算当前元素的值。

枚举类

  • 在某些情况下,一个类的对象是有限且固定的,比如季节类,他只有四个对象。这种实例有限而且固定的类,在Java里称为枚举类。
  • enum关键字(他与class、interface关键字的地位相同)用于定义枚举类。
  • 它与普通类有如下简单区别:
    1. 枚举类可以实现一个或多个接口,使用enum定义的枚举类默认继承了java.lang.Enum类,而不是默认继承Object类,因此枚举类不能显式继承其他父类。其中java.lang.Enum类实现了java.lang.Serializable和java.lang.Comparable两个接口。
    2. 使用enum定义、非抽象的枚举类默认会使用final修饰,因此枚举类不能派生子类。
    3. 枚举类的构造器只能使用private访问控制符,如果省略了控制符,系统会自动添加private。
    4. 枚举类的所有实例必须放在枚举类的第一行显式列出,否则这个枚举类永远都不能产生实例。列出这些实例时,系统会自动添加public static final修饰,无需程序员显式添加。
  • 下面程序定义了一个SeasonEnum枚举类。
public enum SeasonEnum {
    //要注意,如果要实现枚举类里的抽象方法,则每个变量后面应该有一个大括号,内部进行抽象方法的实现
    SPRING,SUMMER,FALL,WINTER;
}
  • 如果需要使用该枚举类的某个实例,则可使用EnumClass.variable的形式:
public class EnumTest {
    public void judge(SeasonEnum s)
    {
        //switch语句里的表达式可以是枚举值
        switch (s)
        {
            case SPRING:
                System.out.println("spring");
                break;
            case SUMMER:
                System.out.println("summer");
                break;
            case FALL:
                System.out.println("fall");
                break;
            case WINTER:
                System.out.println("winter");
                break;
        }
    }
    public static void main(String[] args)
    {
        for(SeasonEnum a : SeasonEnum.values())
        {
            System.out.println(a);
        }
        //使用实例时,可以通过EnumClass.variable来访问
        new EnumTest().judge(SeasonEnum.SPRING);
    }
}
  • java.lang.Enum类中提供了如下几个方法:
int compareTo(E o);//该方法用于与指定枚举对象比较顺序,同一个枚举实例只能与相同类型的枚举实例进行比较。如果该枚举对象位于指定枚举对象之后,则返回正整数;如果该枚举对象位于指定枚举对象之前,则返回负整数,否则返回0
String name();//返回此枚举实例的名称,这个名称就是定义枚举类时列出的所有枚举值之一。与此方法相比,大多数程序员应优先考虑toString()方法,因为后者返回更加用户友好的名称
int ordinal();//返回枚举值在枚举类中的索引值(就是枚举值在枚举声明中的位置,第一个枚举值的索引值为0)
String toString();//返回枚举常量的名称,与name方法相似,但toString方法更常用
public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name);//这是一个静态方法,用于返回指定枚举值中指定名称的枚举值。名称必须与在该枚举值中声明枚举值时所用的标识符完全匹配,不允许使用额外的空白字符。

对象与垃圾回收

对象在内存中的状态
  • 可达状态:当一个对象被创建后,若有一个以上的引用变量引用他,这该对象在程序中处于可达状态,程序可以通过引用变量来调用该对象的实例变量和方法
  • 可恢复状态:如果程序中某个对象不再有任何引用变量引用它,就进入了可恢复状态,在这种状态下,系统的垃圾回收机制准备回收该对象所占用的内存。在回收前,系统将调用所有可恢复状态对象的finalize()方法进行资源清理。如果系统在调用该方法时有一个引用变量引用了该对象,则这个对象会再次变为可达状态;否则该对象将进入不可达状态
  • 不可达状态:当对象与所有引用变量的关联都被切断时进入该状态。此时系统才会真正回收该对象所占有的资源。
对象的软、虚、弱引用
  1. 强引用:最常见的方式。处于可达状态。
  2. 软引用:需要通过SoftReference实现,此时对象有可能被垃圾回收机制回收,根据内存空间决定。
  3. 弱引用:需要通过WeakReference实现,此时不管内存状态,对象都会被垃圾回收机制运行时回收。
  4. 虚引用:需要通过PhantomReference实现,他和没有引用的效果类似,他主要用于跟踪对象被垃圾回收的状态,它不能单独使用,必须和引用队列(ReferenceQueue)联合使用。
  • 上面三个引用类都包含了一个get()方法,用于获取它们所引用的对象。

修饰符的适用范围

  • 以下为Java修饰符适用范围总表
外部类、接口成员属性方法构造器初始化块成员内部类局部成员
public
protected
包访问控制符--
private
abstract
final
static
strictfp
synchronized
native
transient
volatile
default
  • strivtfp就是精确浮点,可以使浮点运算更加精确。
  • native关键字主要用于修饰一个方法,使用native修饰的方法类似于一个抽象方法,但是他通常需要C语言实现。

Java9 的多版本JAR包

  • 使用JAR包有以下好处:
    1. 安全。能够对JAR文件进行数字签名,只让能够识别数字签名的用户使用里面的东西。
    2. 加快下载速度
    3. 压缩
    4. 包封装
    5. 可移植性。JAR包作为内嵌在Java平台内部处理的标准,能够在各种平台上直接使用。
jar命令详解
  • 创建JAR文件:jar cf test.jar -C dist/ .
  • 创建JAR文件,并显示压缩过程:jar cvf test.jar -C dist/ .
  • 不使用清单文件:jar cvfM test.jar -C dist/ .
  • 自定义清单文件内容:jar cvfm test.jar manifest.mf -C dist/ .
  • 查看JAR包内容:jar tf test.jar
  • 查看JAR包详细内容:jar tvf test.jar
  • 解压缩:jar xf test.jar
  • 带提示信息解压缩:jar xvf test.jar
  • 更新JAR文件:jar uf test.jar Hello.class
  • 更新时显示详细信息:jar uvf test.jar Hello.class
  • 创建多版本JAR包:jar cvf test.jar -C dist7/ . --release 9 -C dist/ .
创建可执行的JAR包
jar cvfe test.jar test.Test test
  • 上述命令表示把test目录下的所有文件都压缩到test.jar包中,并指定使用test.Test类作为程序的入口。
  • 运行上面的JAR包有两种方式
    1. 使用java命令,java -jar test.jar
    2. 使用javaw命令,javaw test.jar
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lanciberrr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值