面向对象(下)——Java教案(五)
文章目录
- 1.包装类
- 自动装箱拆箱
- 基本类型与字符串类型的转换
- 包装类的比较
- 与数值类型的值进行比较
- 包装类与包装类的比较
- 通过自动装箱比较
- compare比较包装类
- 2.处理对象
- 打印对象和toString方法
- ==和equals()方法
- 用法对比
- “hello”和new String("hello")
- equals()方法
- 重写equals()方法
- 重写equals()方法的要求
- 案例题
- 3.类成员
- 类成员的基本介绍
- 单例类
- final修饰符
- final变量
- final成员变量
- final局部变量
- final修饰基本变量和引用变量区别
- 可执行宏替换的final变量
- 深入宏替换
- final方法
- final类
- 不可变类
- 介绍
- 创建规则
- 缓存实例的不可改变类(有空了解)
- 4.抽象类
- 为什么用抽象类?
- 抽象方法和抽象类
- 抽象类举例
- abstract和其他修饰符的爱恨情仇
- 抽象类的使用
- 5.接口
- 定义
- 接口继承
- 使用接口
- 接口和抽象类
- 面向接口编程
- 案例
- 6.内部类
- 非静态内部类
- 静态内部类
- 使用内部类
- 在外部类内部使用内部类
- 外部类以外使用非静态内部类
- 在外部类以外使用静态内部类
- 局部内部类
- 7.Lambda表达式
- Lambda的组成
- Lambda表达式与函数式接口
- 方法引用和构造器引用
- 引用类方法
- 引用特定对象的实例
- 引用某类对象的实例方法
- 引用构造器
- Lambda表达式与其他方式对比
- Lambda表达式与内部类的联系和区别
- 使用Lambda表达式调用Arrays的类方法
- 8.枚举类
- 介绍
- 入门
- 枚举类的成员,方法和构造器
- 实现接口的枚举类
- 包含抽象方法的枚举类
- 9.对象与垃圾回收
- 对象在内存存在的状态
- 强制垃圾回收
- 扩展学习
- 综合练习
- 学生管理系统
1.包装类
java提供了8种数据类型,但是这八种数据类型不支持面向对象的编程机制。
制约:所有引用类型的变量都继承了Object类,可以当成Object类型的变量使用。但是基本数据类型的变量不可以,如果某个方法需要Object类型的参数,但实际需要的时2,3这样的数值,就比较难处理。
为了解决这八种数据类型不能当成Object类型变量使用的问题,java提供了包装类。为八种基本类型都提供了相应的引用类型,称之为基本数据类型的包装类。
自动装箱拆箱
jdk1.5提供了自动装箱和自动拆箱的功能,允许把基本类型值直接赋给对应的包装类引用变量,也允许把包装类对象直接赋给对应的基本类型变量。
package baozhuanglei;public class TestOne {public static void main(String[] args) {Integer inObj = 5;int it = inObj;System.out.println(it);Object boolObj = 123456;if(boolObj instanceof Boolean) {boolean b = (Boolean) boolObj;System.out.println(b);}}}
自动装箱与自动拆箱大大简化了基本类型变量和包装类对象之间的转换过程。
注意:
- 自动装箱和自动拆箱一定要注意类型匹配
- Integer只能自动拆箱成int,int只能自动装箱成Integer(即使赋值给Objecct对象,也是利用了java的自动向上转型特性),不要试装箱成Boolean对象。
通过包装类,可以将基本数据类型变量当成对象使用,也可以将包装类当成基本数据类型变量使用。
基本类型与字符串类型的转换
包装类还可以实现j基本类型变量和字符串之间的转换。把字符串类型转换为基本数据类型的两种方式。
- 包装类提供的parseXXX(String s)静态方法(除Character之外的所有包装类都提供了此方法)。
- 利用包装类提供的valueOf(String s)静态方法
方法一:
package baozhuanglei;public class TestTwo {public static void main(String[] args) {String intStr = "123";System.out.println(Integer.parseInt("12365"));// 把一个特定字符串转换为int变量int it1 = Integer.parseInt(intStr);int it2 = Integer.valueOf(intStr);System.out.println(it1);System.out.println(it2);String floatStr ="4.56";//把一个特定字符串转换成float变量float ft1 = Float.parseFloat(floatStr);float ft2 = Float.valueOf(floatStr);System.out.println(ft1);System.out.println(ft2);//把一个float变量转换成String变量String fsStr = String.valueOf(2.456f);System.out.println(fsStr);String sdStr = String.valueOf(3.344);System.out.println(sdStr);//把一个boolean变量转换成String变量String boolStr = String.valueOf(true);System.out.println(boolStr.toUpperCase());// 把一个char变量转换为String变量char a = 100;String s = String.valueOf(a);System.out.println(s);}}
方法二:
將基本数据类型转换为字符串类型最简单的方法
在变量末尾加双引号。
String a = 123+"";
包装类的比较
与数值类型的值进行比较
虽然包装类是引用数据类型,但是包装类的实例可以与数值类型的值进行比较,这种比较是直接取出包装类实例所包装的数值进行比较的。
Integer n = Integer.valueOf(6);if(n>=5) {System.out.println("n大于5");}
包装类与包装类的比较
包装类实例的实际是引用数据类型,只有两个包装类指向同一个对象时才会返回true
//比较两个包装类的实例是否相等System.out.println(Integer.valueOf(2)==Integer.valueOf(6));//false
通过自动装箱比较
Integer ita = 2;Integer itb = 2;Integer ite = 3;Integer itc = 128;Integer itd = 128;1. System.out.println(ita==itb);//true2. System.out.println(ita==itc);//false3. System.out.println(itc==itd);//false
从上面代码我们可以看出,1和3比较的都是相等的一个数字,但是却出现了不一样的结果,这是为何呢?
static {// high value may be configured by propertyint h = 127;String integerCacheHighPropValue =sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");if (integerCacheHighPropValue != null) {try {int i = parseInt(integerCacheHighPropValue);i = Math.max(i, 127);// Maximum array size is Integer.MAX_VALUEh = Math.min(i, Integer.MAX_VALUE - (-low) -1);} catch( NumberFormatException nfe) {// If the property cannot be parsed into an int, ignore it.}}high = h;cache = new Integer[(high - low) + 1];int j = low;for(int k = 0; k < cache.length; k++)cache[k] = new Integer(j++);// range [-128, 127] must be interned (JLS7 5.1.7)assert IntegerCache.high >= 127;}private IntegerCache() {}}
从系统代码中可以看出,系统把一个-128 ~ 127的整数自动装箱成一个Integer实例,实际上是直接指向对应的数组,因此-128 ~ 127之间的同一个整数自动装箱成Integer实例,永远是引用cache数组的同一个数组,所以他们全部相等,但每次把一个不在-128~127之间的整数自动装箱成Integer实例时,系统会重新创建一个Integer实例,所以不相等。
compare比较包装类
java7
包装类.compare(a,b)
a>b 返回1
a=b 返回0
a<b 返回-1
true>false
System.out.println(Integer.compare(1, 100));//-1
System.out.println(Integer.compare(400,20));//1
System.out.println(Integer.compare(1,1));//0
System.out.println(Boolean.compare(true, false));//1
System.out.println(Boolean.compare(true, true));//0
System.out.println(Boolean.compare(false, true));//-1
布尔类型进行比较时true>false
不仅如此,Java 7还为Character 包装类增加了大量的工具方法来对一个字符进行判断。关于Character中可用的方法请参考Character的API文档。
java8
package baozhuanglei;public class TestThree {public static void main(String[] args) {byte b=-3;//将byte类型转换成无符号的整数System.out.println(Byte.toUnsignedInt(b));//253System.out.println(String.valueOf(b));//-3//指定使用十六进制解析无符号整数int val = Integer.parseUnsignedInt("ab",16);System.out.println(val);//171//将-12转换为无符号int类型,然后转换为16进制的字符串System.out.println(Integer.toUnsignedString(-12,16));//fffffff4//将两个数转换为无符号整数后相除System.out.println(Integer.divideUnsigned(-2, 3));//1431655764//将两个数转换为无符号整数相除后求余System.out.println(Integer.remainderUnsigned(-2, 7));//2}}
2.处理对象
java都是Object类的实例,都可以使用该类中定义的方法,这些方法提供了处理Java对象通用的一些方法。
打印对象和toString方法
toString()方法是Object类里的一个实例方法,所有的Java类都是Object类的子类,因此所有的Java对象都具有toString()方法。
package dayinduixiang;
class Person{private String name;public Person(String name) {this.name=name;}
}public class TestOne {public static void main(String[] args) {Person p = new Person("张三");System.out.println(p);//dayinduixiang.Person@15db9742System.out.println(p.toString());//dayinduixiang.Person@15db9742//当直接打印对象的实例时,返回的是对象的toString方法的返回值,所以上面结果一样}}
toString()方法是一个自我描述的方法。功能:程序员打印该对象时,系统输出对象的自我信息。
Object提供的toString方法总是返回该对象实现类的“类名+@+hashCode”值,这个返回值并不是真正的“自我描述的功能”,必须重写Object类的toString()方法。
package dayinduixiang;class Apple{private String color;private double weight;public Apple(String color,double weight) {this.color=color;this.weight=weight;}//重写toString()方法@Overridepublic String toString() {return "苹果的颜色是"+color+"苹果的重量是"+weight;}}public class TestTwo {public static void main(String[] args) {Apple p = new Apple("黄色",2.14);System.out.println(p);//苹果的颜色是黄色苹果的重量是2.14System.out.println(p.toString());//苹果的颜色是黄色苹果的重量是2.14}}
通常toString的返回格式,运用字符串拼接
类名[field1=值1,field2=值2];
//通常的返回格式public String toString() {return "Apple[color="+color+",weight="+weight+"]";}
==和equals()方法
判断两个变量相等的两种方式
- 使用==
- 使用equals()方法。
用法对比
- 如果两个变量时基本数据类型,且都是数值类型(数据类型不一定相同),两个变量相等,返回true。
- 两个引用类型的变量,只有他们指向同一个对象时,==判断才会true。==不可用于比较类型上没有父子关系的两个对象。
package bijiao;public class Son {public static void main(String[] args) {//基本数据类型比较int it =65;float fl = 65.0f;System.out.println("65和65.0f是否相等"+(it==fl));//truechar ch ='A';System.out.println("65和'A'是否相等"+(it==ch));//true//引用数据类型比较由于str1和str2属于两个不同的对象,所以==比较时不相等。String str1 = new String("123");String str2 = new String("123");System.out.println(str1==str2);//falseSystem.out.println(str1.equals(str2));//trueSystem.out.println("hello".equals(new String("hello")));//trueSystem.out.println(str1==new String("123"));//false// 由于java.lang.String和Son类没有继承关系,所以下面代码报错
// System.out.println("hello"==new Son());}
}
public class PersonMain {public int age;public String name;public static void main(String[] args) {PersonMain person = new PersonMain();person.name = "zhangsan";person.age = 14;PersonMain person2 = new PersonMain();person2.name = "zhangsan";person2.age = 14;System.out.println(person);System.out.println(person2);}}
“hello”和new String(“hello”)
当java直接使用"hello"的字符串直接量(包括在编译时就计算出来的字符串值)时,jvm将会使用常量池来管理这些字符串;
当使用new String(“hello”)时,JVM会先使用常量池来管理“hello”直接量,再调用String构造器创造一个新的String对象。新创建的对象保存在堆内存中。相当于new String(“hello”)长生了两个字符串对象。
package bijiao;public class One {public static void main(String[] args) {String s1 = "疯狂Java";String s2 = "疯狂";String s3 = "Java";//s4后面的字符串值在编译时已经确定下来,//s4直接饮用变量池中的“疯狂Java”;String s4 = "疯狂"+"Java";//s5后面的字符串值在编译时已经确定下来,//s5直接饮用变量池中的“疯狂Java”;String s5 = "疯"+"狂"+"Java";//s6后面的字符串不能在编译时就确定下来//不能引用常量池中的字符串String s6 = s2+s3;//使用构造器创建一个String对象//s7引用堆内存中新创建的String对象String s7 = new String("疯狂Java");System.out.println(s1==s4);//trueSystem.out.println(s1==s5);//trueSystem.out.println(s1==s6);//falseSystem.out.println(s1==s7);//falseSystem.out.println(s1.equals(s4));//trueSystem.out.println(s1.equals(s5));//trueSystem.out.println(s1.equals(s6));//trueSystem.out.println(s1.equals(s7));//true}}
JVM常量池保证相同字符串的直接量只有一个,不会产生多个副本。例如s1,s2,s3,s4在编译时期就能确定下来,因此他们都将引用常量池中的同一个字符串对象。
使用new String()创建的字符串对象是运行时创建出来的,它被保存在运行时内存区(即堆内存),不会放入常量池中。
在很多时候,程序判断两个引用变量是否相等时,希望“值相等”,并不严格要求两个引用变量指向同一个对象。例如两个字符串变量,只要求引用字符串对象中保存的值相等即可认为相等。此时就可以使用equals()方法判断
equals()方法
equals()方法是Object提供的实例方法,可供所有类使用,但是正常情况下equals()方法和==相同,没有任何差别,所以需要时需要重写equals()方法。
注:String()类已经重写了equal()方法,所以当比较两个值相同的字符串时,返回true。
例如:
String ss1 = new String("123");String ss2 = new String("123");System.out.println(ss1.equals(ss2));//trueSystem.out.println(ss1==ss2);//false
package bijiao;
class Person{public boolean equals(Object obj) {//不加判断,总是返回true//即任何对象都和Person实例相等return true;}}//定义一个Dog的空类
class Dog{}public class Two {public static void main(String[] args) {Person p = new Person();Dog d = new Dog();System.out.println(p.equals(d));//trueSystem.out.println(p.equals(new String("123")));//true}
}
从上面代码中我们可以看出,equals()的用法我们可以随意的修改。
重写equals()方法
大部分时候,并不希望看到Person对象和Dog对象相等的“荒唐局面”,还是希望两个类型相同的对象才可能相等,并且关键的成员变量相等才能相等。
package bijiao;class Person {private String name;private String idStr;public Person(String name, String idStr) {this.name = name;this.idStr = idStr;}public void setName(String name) {this.name = name;}public String getName() {return this.name;}public void setIdStr(String idStr) {this.idStr = idStr;}public String getIdStr() {return this.idStr;}public boolean equals(Object obj) {// 如果两个对象为一个对象,返回trueif (this == obj) {return true;}//判断obj所属的类是否是Person类的实例if(obj!=null&&obj.getClass()==Person.class) {Person personObj = (Person)obj;//并且当当前对象的idStr与obj对象的idStr相等时才可判断俩对象相等if.equals(personObj.getIdStr())) {return true;}}return false;}}// 定义一个Dog的空类
class Dog {}public class Two {public static void main(String[] args) {Person p1 = new Person("孙悟空","12343433433");Person p2 = new Person("孙行者","12343433433");Person p3 = new Person("孙悟饭","99933466");System.out.println(p1.equals(p2));//trueSystem.out.println(p1.equals(p3));//false//Person.class Person类的实例System.out.println(Person.class); //class bijiao.Person//查看p1的所属类System.out.println(p1.getClass());//class bijiao.Person}}
改善equals方法
- 同源的判断
- 成员变量非null的判断
package lession11_22;class PersonDemo {private String name;private String idStr;public PersonDemo(String name, String idStr) {this.name = name;this.idStr = idStr;}public void setName(String name) {this.name = name;}public String getName() {return this.name;}public void setIdStr(String idStr) {this.idStr = idStr;}public String getIdStr() {return this.idStr;}public boolean equals(Object obj) {// 如果两个对象为一个对象,返回trueif (this == obj) {return true;}//判断obj所属的类是否是PersonDemo类的实例if(obj!=null) {Dog PersonDemoObj = (Dog)obj;if(PersonDemoObj instanceof PersonDemo ) {//并且当当前对象的idStr与obj对象的idStr相等时才可判断俩对象相等
// System.out.println("测试null");
// System.out.println(this.getIdStr());if(this.getIdStr()!=null) {if(this.getIdStr().equals(PersonDemoObj.getIdStr())&&this.name.equals(PersonDemoObj.getName())) {return true;}}}}return false;}}// 定义一个Dog的空类
class Dog extends PersonDemo{public Dog(String name, String idStr) {super(name, idStr);// TODO Auto-generated constructor stub}
}public class Two {public static void main(String[] args) {PersonDemo p1 = new PersonDemo("孙悟空","12343433433");Dog dog = new Dog("孙悟空","12343433433");
// PersonDemo p2 = new PersonDemo("孙悟空","12343433433");
// PersonDemo p3 = new PersonDemo("孙悟饭","99933466");System.out.println(p1.equals(dog));//true
// System.out.println(p1.equals(p3));//false//PersonDemo.class PersonDemo类的实例
// System.out.println(PersonDemo.class); //class bijiao.PersonDemo
// //查看p1的所属类
// System.out.println(p1.getClass());//class bijiao.PersonDemo}}
重写equals()方法的要求
案例题
长方形和正方形形都属于几何图形,都有周长,面积,体积,并且它们都有自己的周长,面积,体积计算公式。使用已学的的知识设计一个程序,可以计算不同图形的面积,周长,体积。
(1)定义Shape作为父类,具有长和宽的属性,并在类中定义方法求周长,面积,以及体积。
(2)定义Shape子类正方形(Square),重写父类的方法。并重写toString()和equals()方法。
要求:toString()打印正方形的面积,当两个对象的长和宽的值相等时,使用equals方法可以返回true。
(3)定义Shape子类长方形(Rectangle),重写父类的方法。并重写toString()和equals()方法。
要求:toString()打印长方形的体积,当两个对象的体积和面积的值相等时,使用equals方法可以返回true。
(4)创建测试类TestShape类,在其main()方法中输出正方形的周长,面积,体积,以及使用它的equals和toString方法,长方形同理。
注意:代码中要体现近期所学内容。
package lession11_23;class Shape {private double length;private double wide;public Shape(double length, double wide) {super();this.length = length;this.wide = wide;}public double getLength() {return length;}public void setLength(double length) {this.length = length;}public double getWide() {return wide;}public void setWide(double wide) {this.wide = wide;}public double getCircumference() {return 0;}public double getArea() {return 0;}public double getVolume() {return 0;}// public static double computedCircumference(double length, double wide) {// return 2*(length+wide);
// }
//
// public static double computedArea(double length, double wide) {// return length*wide;
//
// }
// public static double computedVolume(double length, double wide, double height) {// return length*wide*height;
//
// }}class Rectangle extends Shape {private double height;private double volume;private double area;public double getHeight() {return height;}public void setHeight(int height) {this.height = height;}public Rectangle(double length, double wide, double height) {super(length, wide);this.height = height;this.area = length*wide;this.volume = length*wide*height;//使用static
// this.area = Shape.computedArea(length, wide);
// this.volume = Shape.computedVolume(length, wide, height);}public double getCircumference() {return 2 * (this.getLength() + this.getWide());//方式二// return Shape.computedCircumference(this.getLength(), this.getWide());}public double getArea() {return this.area;}public double getVolume() {return this.volume;}public String toString() {return "Rectangle的体积是" + this.getVolume();}public boolean equals(Object obj) {if (obj == this) {return true;}if (obj != null) {if (obj instanceof Shape) {Rectangle rectangle = (Rectangle) obj;if((int)this.area==(int)rectangle.area&&(int)this.volume==(int)rectangle.volume) {return true;}}}return false;}}
class Square extends Shape {private double height;private double volume;private double area;public Square(double length, double wide, double height) {super(length, wide);this.height = height;this.area = length*wide;this.volume = length*wide*height;// TODO Auto-generated constructor stub//方式二:
// this.area = Shape.computedArea(length, wide);
// this.volume = Shape.computedVolume(length, wide, height);}public double getHeight() {return height;}public void setHeight(int height) {this.height = height;}public double getCircumference() {return 2 * (this.getLength() + this.getWide());//方式二
// return Shape.computedCircumference(this.getLength(), this.getWide());}public double getArea() {return this.area;}public double getVolume() {return this.volume;}public String toString() {return "Square的面积是" + this.getArea();}public boolean equals(Object obj) {if (obj == this) {return true;}if (obj != null) {if (obj instanceof Shape) {Square square = (Square) obj;if((int)this.getWide()==(int)square.getWide()&&(int)this.getLength()==(int)square.getLength()) {return true;}}}return false;}}public class TestShap {public static void main(String[] args) {System.out.println("------------正方形-------------");Shape square1 = new Square(1.5, 2.5, 3.5);Shape square2 = new Square(1.5, 2.5, 3.0);System.out.println("周长:"+square1.getCircumference());System.out.println("面积:"+square1.getArea());System.out.println("体积:"+square1.getVolume());System.out.println("toString打印:"+square1.toString());System.out.println("equals返回:"+square1.equals(square2));System.out.println("------------长方形-------------");Shape rect1 = new Rectangle(1.0, 2.0, 3.0);Shape rect2 = new Rectangle(1.0, 2.0, 3.1);System.out.println("周长:"+rect1.getCircumference());System.out.println("面积:"+rect1.getArea());System.out.println("体积:"+rect1.getVolume());System.out.println("toString打印:"+rect1.toString());System.out.println("equals返回:"+rect1.equals(rect2));}}
3.类成员
类成员的基本介绍
static修饰
static修饰的成员为类成员,类成员有类变量,类方法,静态初始化块。static不能修饰构造器。static修饰的成员属于整个类,不属于单个实例
在java类里面只能包含成员变量,方法,构造器,初始化块,内部类(包括接口、枚举)5种成员。
其中static可以修饰成员变量,方法,初始化块,内部类(包括接口、枚举)
类的生存
类变量属于整个类,当系统第一次准备使用该类时,系统会为该类变量分配内存空间,类变量开始生效,直到该类被卸载,该类的类变量所占有的内存才被系统的垃圾回收机制回收。类变量生存范围几乎等同于该类的生存范围。当类初始化完成后,类变量也被初始化完成。
类的访问
类变量可以通过类访问,也可以通过类的实例访问,但是通过实例访问是一种假象,对象根本不拥有类变量。当系统通过类的实例访问类变量时,java底层自动把类的实例转换为类来访问。
类方法同类变量一样,也可以通过类的实例访问。所以即使类的实例为空,也可以通过实力访问,因为java底层自动把类的实例转换为类来访问。
package lei;public class One {public static void eat() {System.out.println("吃");}public static void main(String[] args) {One o = null;o.eat(); //吃}
}
可以访问,说明空对象可以访问它的类成员
对static关键字而言,有一条非常重要的规则:类成员(包括方法、初始化块、内部类和枚举类)不能访问实例成员(包括成员变量、方法、初始化块、内部类和枚举类)。因为类成员是属于类的,类成员的作用域比实例成员的作用域更大,完全可能出现类成员已经初始化完成,但实例成员还不曾初始化的情况,如果允许类成员访问实例成员将会引起大量错误。
单例类
正常情况下,类构造器权限为public,允许其他类自由创建该类实例。在某些情况下,不允许自由创建该类的对象,而只允许该类创建一个对象。为避免其他类创建该类的实例,把该类的构造器设置成private。这样的类被称为单例类
根据良好封装的原则:
一旦把该类的构造器隐藏起来,就需要提供一个public方法作为该类的访问点,用于创建该类的对象,且该方法必须使用static修饰(因为调用该方法之前还不存在对象,因此调用该方法的不可能是对象,只能是类)。
除此之外,该类还必须缓存已经创建的对象,否则该类无法知道是否曾经创建过对象,也就无法保证只创建一个对象。为此该类需要使用一个成员变量来保存曾经创建的对象,因为该成员变量需要被上面的静态方法访问,故该成员变量必须使用static修饰。
package lei;class Singleton{//创建一个类变量来缓存曾经创建的实例private static Singleton instance;//使用private修饰构造器,隐藏private Singleton() {}//提供一个静态方法,返回Singleton()实例//该方法可加入自定义控制,保证只产生一个Singleton对象public static Singleton getInstance() {//如果instance为null,说明没有Singleton实例//如果instance不为null,说明已经创造了一个Singleton实例//将不会再创造instance实例if(instance==null) {return instance = new Singleton();}return instance;}}
public class Two{public static void main(String[] args) {Singleton s1 = Singleton.getInstance();Singleton s2 = Singleton.getInstance();System.out.println(s1==s2);//true 说明s1和s2指向同一个对象}
}
正是通过上面getInstance方法提供的自定义控制(这也是封装的优势:不允许自由访问类的成员变量和实现细节,而是通过方法来控制合适暴露),保证Singleton类只能产生一个实例。所以,在SingletonTest类的main()方法中,看到两次产生的Singleton对象实际上是同一个对象。
final修饰符
final关键字可用于修饰类,变量和方法。类似于c#中的sealed关键字,用于修饰的类,变量,方法不可改变。
final变量
final修饰变量时,表示该变量一旦赋值,就不可改变。final可修饰成员变量(类变量,实例变量),局部变量,形参。
final修饰的变量不可改变改变。一旦获得初始值,该final变量的值就不能被重新赋值。(有的书上说不能赋值,这种说法不准确)
final成员变量
final修饰的变量必须由程序员显示的指定初始值。
final修饰的类变量、实例变量能指定初始值的地方如下。
类变量:必须在静态初始化块中指定初始值或声明该类型变量时指定初始值。而且只能在其一指定。
实例变量:必须在非静态初始化块、声明该实例变量或构造器中指定初始值。而且只能在其一指定。
package lei;public class Three {final int a = 1;//在构造器或者初始化块中赋值final String str;final int c;final static double d;//final int e;{//在非静态代码块中赋值,允许str ="Hello";//a声明变量时已经有了默认值,不能重新赋值//a=5;}static {//静态初始化块d=3.14;}//构造器public Three() {c=100;}public void change() {//方法中不允许给final类型的变量赋值//e=100;}
}
如果打算在初始化块或者构造方法中为final成员变量赋值,则不要在赋值之前访问final成员变量。
如:
final局部变量
final修饰的变量必须由程序员显示的指定初始值。
final局部变量可以在声明数据类型时赋值,也可以在之后的代码中赋值。但只能赋值一次
package lei;public class Four {public void test(final int a) {//产生实例对象时,会为a赋值,所以a不能够再次赋值//a=100;报错System.out.println(a);}public static void main(String[] args) {Four f = new Four();f.test(50);final String a = "100";//a="200"; 非法final int b;b=100;//b=1000;非法}}
final修饰基本变量和引用变量区别
修饰基本类型变量:基本类型变量赋值后不能重新赋值,因此基本类型变量不能够改变。
修饰引用类型变量:引用类型变量,他保存的仅仅是一个引用,final只保证这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生变化。
package lei;
import java.util.Arrays;
class Person{private int age;public Person(int age) {this.age=age;}public void setAge(int age) {this.age=age;}public int getAge() {return this.age;}
}
public class Six {public static void main(String[] args) {//final修饰数组变量 iArr是一个引用变量final int[] iArr = {5,7,6,8};System.out.println(Arrays.toString(iArr));//对数组元素进行排序,合法Arrays.sort(iArr);System.out.println(Arrays.toString(iArr));//对数组元素重新赋值,合法iArr[2]=100;System.out.println(Arrays.toString(iArr));//对iArr重新赋值 不合法//iArr = null;final Person a = new Person(18);System.out.println(a.getAge());//对实例对象a的age变量重新赋值,合法a.setAge(20);System.out.println(a.getAge());//对 a重新赋值 不合法//a=null;}}
从上面可以看出,final修饰的引用类型的变量不能重新赋值,但是可以修改引用对象的值。
课堂演示:
public class Person {int a = 0;public static void main(String[] args) {//不使用final
// Person s = new Person();
// System.out.println(s);
// Person b = new Person();
// System.out.println(b);
// s=b;
// System.out.println(s);//使用final,不能更改对象的地址值,但是可以更改地址里面所存对象的属性值。
// final Person s = new Person();
// System.out.println(s);
// Person b = new Person();
// System.out.println(b);
// s=b;
// System.out.println(s);final Person s = new Person();System.out.println(s);System.out.println(s.a);s.a=100;System.out.println(s.a);}}
可执行宏替换的final变量
对一个final修饰的变量来说,不管他是类变量、实例变量、还是局部变量,只要满足三个条件就不再是一个变量,而是一个直接量。即常量
- 使用final修饰
- 在定义final时制定了初始值
- 该初始值在编译时就被确定下来,不能更改。
package lei;public class Seven {public static void main(String[] args) {//定义一个普通局部变量final int a = 4;System.out.println(a);}
}
定义了一个初始值为4的final局部变量。对于这个程序来说,变量a其实根本不存在,
当程序执行System.out.println(a);时,相当于执行System.out.println(4)
如果被赋的表达式只是基本的算术表达式或字符串链接运算,没有访问普通变量,调用方法,Java编译器同样会将这种final变量当成“宏变量”处理。
package finalshiyong;public class One {public static void main(String[] args) {final int a = 5+2;final double b = 1.2/3;final String str = "疯狂"+"Java";final String book = "疯狂Java讲义:"+99.0;//下面的book2变量的值因为调用了方法,所以无法在编译时被确定下来final String book2 ="疯狂Java讲义:"+String.valueOf(99.0);System.out.println(book=="疯狂Java讲义:99.0");//trueSystem.out.println(book2=="疯狂Java讲义:+99.0");//falseSystem.out.println(book);System.out.println("疯狂Java讲义:+99.0");}}
a、b、str、book在编译时都可以确定,所以都是宏变量
book和book2的区别:book隐式转换为字符串,book2显式转换为字符串。由于book2的值需要调用String的方法,因此编译时无法确定book2的值,book2不会当成宏变量处理。
程序最后两行代码分别判断book、book2和“疯狂Java讲义:99.0”是否相等。由于book是一个“宏变量”,它将被直接替换成“疯狂Java讲义:99.0",因此 book和“疯狂Java 讲义:99.0”相等,但book2和该字符串不相等。
深入宏替换
package finalshiyong;public class Two {public static void main(String[] args) {String s1="疯狂Java";// s2变量引用的字符串可以在编译时就确定下来//因此s2直接引用常量池中已有的"疯狂Java"字符串String s2="疯狂"+"Java";System.out.println(s1==s2);//trueString str1 ="疯狂";String str2 = "Java";//将str1和str2链接起来String s3 = str1+str2;System.out.println(s1==s3);//false}}
final方法
final修饰的方法不可被重写
Java提供的Object类里就有一一个final 方法: getClass(),因为java不想让其他类重写此方法
注意:final修饰的方法仅仅不能够重写,但是可以重载
如果父类方法的权限是private,那么子类就无法重写,此时子类如果创建一个和父类一样的方法,不是父类方法的重写,而是新创建了一个方法。
final类
final修饰的类不可以有子类,例如java.lang.Math类就是一一个final 类,它不可以有子类。
当子类继承父类时,可以获取父类的信息,可以通过重写父类方法获取父类的细节,这样不安全。为了保证类不可继承,可以使用final修饰这个类。
不可变类
介绍
即创建该类的实例后,该实例的实例变量是不可改变的
java的八个包装类和String类都是不可改变类。创建实例后,实例的实例变量不能改变
如:
Double d = new Double(3.14);String s = new String("张三");
作为参数Double类和String类肯定需要提供实例变量来保存这两个参数,但程序无法修改这两个实例变量的值,因此Double类和String类没有提供修改它们的方法。
创建规则
- 使用private和final修饰符来修饰该类的成员变量。
- 提供带参数构造器,用于根据传入参数来初始化类里的成员变量。
- 仅为该类的成员变量提供getter方法,不要为该类的成员变量提供setter方法,因为普通方法无法修改final修饰的成员变量。
- 如果有必要,重写Object类的hashCode()和equals()方法。equals()方法根据关键成员变量来作为两个对象是否相等的标准,除此之外,还应该保证两个用equals()方法判断为相等的对象的hashCode()也相等。
public class Three {public static void main(String[] args) {String str1 = new String("Hello");String str2 = new String("Hello");System.out.println(str1==str2);//falseSystem.out.println(str1.equals(str2));//trueSystem.out.println(str1.hashCode());//69609650System.out.println(str2.hashCode());//69609650}}
String str1 = new String("Hello");String str2 = new String("Hello");System.out.println(str1==str2);//falseSystem.out.println(str1.equals(str2));//trueSystem.out.println(str1.hashCode());//69609650System.out.println(str2.hashCode());//69609650
不可变类的实例在整个生命周期中永远处于初始化状态,它的实例变量不可改变。
前面介绍final关键字时提到,**当使用final修饰引用类型变量时,仅表示这个引用类型变量不可被重新赋值,但引用类型变量所指向的对象依然可改变。**这就产生了一个问题:当创建不可变类时,如果它包含成员变量的类型是可变的,那么其对象的成员变量的值依然是可改变的一这个不可变类其实是失败的。
package finalshiyong;
class Name{private String firstName;private String lastName;public Name() {}public Name(String firstName,String lastName) {this.firstName=firstName;this.lastName=lastName;}public String getFirstName() {return this.firstName;}public void setFirstName(String firstName) {this.firstName=firstName;}public String getLastName() {return this.lastName;}public void setLastName(String lastName) {this.lastName=lastName;}}
public class Person {private final Name name;public Person(Name name) {this.name=name;}public Name getName() {return name;}public static void main(String[] args) {Name n = new Name("悟空","孙");Person p = new Person(n);System.out.println(p.getName().getFirstName());//悟空n.setFirstName("八戒");System.out.println(p.getName().getFirstName());//八戒}}
上述代码n.setFirstName(“八戒”);修改了Name对象firstName的值,但由于Person类的name实例引用了Name对象,就导致了Person对象的name的firstName会被改变,破坏了Person类设计的初衷。
为了保持Person的不可变性,可以采用以下封装。。
public class Person {private final Name name;public Person(Name name) {this.name=new Name(name.getFirstName(),name.getLastName());}public Name getName() {return new Name(name.getFirstName(),name.getLastName());}public static void main(String[] args) {Person p = new Person(new Name("孙","悟空"));System.out.println(p.getName().getFirstName());}}
此时,Person对象的name的firstName不会被修改。
课堂练习
class Name {private String firstName;private String lastName;public Name() {}public Name(String firstName, String lastName) {this.firstName = firstName;this.lastName = lastName;}public String getFirstName() {return this.firstName;}public void setFirstName(String firstName) {this.firstName = firstName;}public String getLastName() {return this.lastName;}public void setLastName(String lastName) {this.lastName = lastName;}}
//public class Person {// private final Name name;
// public Person(Name name) {// this.name=name;
// }
// public Name getName() {// return name;
// }
// public static void main(String[] args) {// Name n = new Name("悟空","孙");
// Person p = new Person(n);
// System.out.println(p.getName().getFirstName());//悟空
// n.setFirstName("八戒");
// System.out.println(p.getName().getFirstName());//八戒
//
// }
//
//}public class Person {private final Name name;public Person(Name name) {this.name = new Name(name.getFirstName(), name.getLastName());}public Name getName() {// return this.name;return new Name(name.getFirstName(), name.getLastName());}public static void main(String[] args) {Name name =new Name("悟空","孙");Person p = new Person(name);System.out.println(p.getName().getFirstName());name.setFirstName("八戒");p.getName().setFirstName("八戒");System.out.println();}}
缓存实例的不可改变类(有空了解)
4.抽象类
为什么用抽象类?
假如父类animal有一个方法move(),指的是小动物的运动方式,但是不同的动物的运动方式是不一样的,有游的,又跑的。此时父类的代码中move()的方法体没有一个具体的方法体。
我们完全可以不用管move()这个方法,但是假如有一个animal的引用变量,该变量实际引用的是子类的实例,那么我们想调用父类的move()方法,必须通过强制类型转换,这降低了程序的灵活性。
抽象方法和抽象类
- 抽象方法和抽象类必须使用abstract修饰,如果一个类含有抽象方法,他一定是抽象类,抽象类不一定含有抽象方法。
- 抽象类不能被实例化
- 抽象类可以有成员变量,初始化块,构造方法,方法(普通方法和抽象方法都ok),内部类(接口,枚举)5种。
- 抽象类的构造方法不能创建实例,只能被子类的实例调用
- 含有抽象方法的类(直接定义了一个抽象方法,或者继承了一个抽象类,里面含有抽象方法,或者是实现了一个接口,接口中的抽象方法没有实现)指定被定义成抽象类
抽象类举例
public abstract class Shape
{{System.out.println("执行Shape的初始化块...");}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 void setColor(String color){this.color = color;}public String getColor(){return this.color;}
}
抽象类不能用于创建实例,只能当作父类被其他子类继承。
定义一个Triangle类
public class Triangle extends Shape
{// 定义三角形的三边private double a;private double b;private double c;public Triangle(String color , double a, double b , double c){super(color);this.setSides(a , b , c);}public void setSides(double a , double b , double c){if (a >= b + c || b >= a + c || c >= a + b){System.out.println("三角形两边之和必须大于第三边");return;}this.a = a;this.b = b;this.c = c;}// 重写Shape类的的计算周长的抽象方法public double calPerimeter(){return a + b + c;}// 重写Shape类的的返回形状的抽象方法public String getType(){return "三角形";}
}
定义一个Circle类
public class Circle extends Shape
{private double radius;public Circle(String color , double radius){super(color);this.radius = radius;}public void setRadius(double radius){this.radius = radius;}// 重写Shape类的的计算周长的抽象方法public double calPerimeter(){return 2 * Math.PI * radius;}// 重写Shape类的的返回形状的抽象方法public String getType(){return getColor() + "圆形";}public static void main(String[] args){Shape s1 = new Triangle("黑色" , 3 , 4, 5);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());}
}
利用抽象类和抽象方法的优势,可以更好地发挥多态的优势,使得程序更加灵活。
当使用abstract 修饰类时,表明这个类只能被继承;
当使用abstract修饰方法时,表明这个方法必须由子类提供实现(即重写)。
而 final修饰的类不能被继承,final修饰的方法不能被重写。因此 final和 abstract永远不能同时使用。
abstract和其他修饰符的爱恨情仇
final
abstract修饰的类只能被继承,fianl修饰的类为最终类,不能被继承,所以二者不能同时使用。
static
static是修饰类成员的,abstract修饰的方法没有方法体,不可能调用一个没有方法体的方法,所以二者不能同时使用。
变量和构造器:
abstract不能用于修饰变量和构造器,抽象类里面定义的构造器只是普通构造器
package jiekou;abstract class Father {String name;String profession;String from;{from ="中国";}public Father(String name,String profession) {this.name=name;this.profession=profession;}public void drink() {System.out.println("都喜欢和牛奶");}public abstract void eat();}
public class Son extends Father{public Son() {super("张情","程序员");}@Overridepublic void eat() {System.out.println("吃苹果");}public static void main(String[] args) {Son son = new Son();son.eat();son.drink();System.out.println(son.from);}}
注意:
- abstract不能用于修饰成员变量,不能用于修饰局部变量,即没有抽象变量、没有抽象成员变量等说法;abstract也不能用于修饰构造器,没有抽象构造器,抽象类里定义的构造器只能是普通构造器。
- static和 abstract并不是绝对互斥的,static和abstract虽然不能同时修饰某个方法,但它们可以同时修饰内部类。
- abstract关键字修饰的方法必须被其子类重写才有意义,否则这个方法将永远不会有方法体,因此 abstract方法不能定义为private访问权限,即 private和abstract 不能同时修饰方法
抽象类的使用
抽象类不能创建实例,只能当成父类来被继承。抽象类是从多个具体类中抽象出来的父类,它具有更高层次的抽象。从多个具有相同特征的类中抽象出一个抽象类,以这个抽象类作为其子类的模板,从而避免了子类设计的随意性。
抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会大致保留抽象类的行为方式。
如果编写一个抽象父类,父类提供了多个子类的通用方法,并把一个或多个方法留给其子类实现,这就是一种模板模式,模板模式也是十分常见且简单的设计模式之一。
案例:
父类中,父类的普通方法依赖于一个抽象方法,而抽象方法则推迟到子类中提供实现。
假如父类普通方法不依赖子类
问题:代码冗余
package lession11_24;/*** @Classname ss* @Description TODO* @Date 2021/11/24 10:18* @Created by DELL*/
abstract class SpeedMeter {// 转速private double turnRate;public SpeedMeter() {}// 把计算车轮周长的方法定义成抽象方法public abstract double calGirth();public void setTurnRate(double turnRate) {this.turnRate = turnRate;}public double getTurnRate() {return this.turnRate;}// 定义计算速度的通用算法public abstract double getSpeed();}public class CarSpeedMeter extends SpeedMeter {private double radius;public CarSpeedMeter(double radius) {this.radius = radius;}public double calGirth() {return radius * 2 * Math.PI;}@Overridepublic double getSpeed() {return calGirth() * getTurnRate();}public static void main(String[] args) {CarSpeedMeter csm = new CarSpeedMeter(0.34);csm.setTurnRate(15);System.out.println(csm.getSpeed());}
}
改进之后:代码精简
abstract class SpeedMeter
{// 转速private double turnRate;public SpeedMeter(){}// 把计算车轮周长的方法定义成抽象方法public abstract double calGirth();public void setTurnRate(double turnRate){this.turnRate = turnRate;}// 定义计算速度的通用算法public double getSpeed(){// 速度等于 周长 * 转速return calGirth() * turnRate;}
}
public class CarSpeedMeter extends SpeedMeter
{private double radius;public CarSpeedMeter(double radius){this.radius = radius;}public double calGirth(){return radius * 2 * Math.PI;}public static void main(String[] args){CarSpeedMeter csm = new CarSpeedMeter(0.34);csm.setTurnRate(15);System.out.println(csm.getSpeed());}
}
SpeedMeter类里提供了速度表的通用算法,但一些具体的实现细节则推迟到其子类CarSpeedMeter类中实现。这也是一种典型的模板模式。
- 抽象父类可以只定义需要使用的某些方法,把不能实现的部分抽象成抽象方法,留给其子类去实现。
- 父类中可能包含需要调用其他系列方法的方法,这些被调方法既可以由父类实现,也可以由其子类实现。父类里提供的方法只是定义了一个通用算法,其实现也许并不完全由自身实现,而必须依赖于其子类的辅助。
5.接口
将抽象类抽象的更加彻底。特殊的抽象类——接口。
接口中没有普通方法,都是抽象方法。Java8对其进行了改进,允许接口中定义默认方法,默认方法可以提供方法实现。
对于不同型号的主机板而言,它们各自的PCI插槽都需要遵守一个规范,遵守这个规范就可以保证插入该插槽里的板卡能与主机板正常通信。对于同一个型号的主机板而言,它们的PCI插槽需要有相同的数据交换方式、相同的实现细节,它们都是同一个类的不同实例。
类是一种具体实现体。而接口定义了一种规范。
接口定义了某一批类所需要遵守的规范,接口不关心这些类的内部状态数据,也不关心这些类里方法的实现细节,它只规定这批类里必须提供某些方法,提供这些方法的类就可满足实际需要。
设计思想:
接口是从多个相似类中抽象出来的规范,接口不提供任何实现。接口体现的是规范和实现分离的设计哲学。
让规范和实现分离正是接口的好处,让软件系统的各组件之间面向接口耦合,是一种松耦合的设计。例如主机板上提供了PCI插槽,只要一块显卡遵守PCI接口规范,就可以插入PCI插槽内,与该主机板正常通信。至于这块显卡是哪个厂家制造的,内部是如何实现的,主机板无须关心。
软件系统的各模块之间也应该采用这种面向接口的耦合,从而尽量降低各模块之间的耦合,为系统提供更好的可扩展性和可维护性。
接口定义的是多个类共同的公共行为规范,这些行为是与外部交流的通道,这就意味着接口里通常是定义一组公用方法。
定义
【修饰符】 interface 接口名 extends 接口1,接口2{
零个到多个常量定义 .. .
零个到多个抽象方法定义.. .
零个到多个内部类、接口、枚举定义.. .
零个到多个私有方法(java9)、默认方法或类方法定义 .. .
}
接口的特点
- 接口没有代码块和构造方法
- 修饰符只能是public 或者是空。空表示只能在本包使用
- 接口中定义的普通方法默认都是使用public abstract修饰
- 接口种定义的变量默认都是 public static final修饰。并且都得有默认值。
- 接口只能继承接口,不能继承类。可以一次继承多个接口
- 子类实现了接口,如果这个接口继承了多个接口,那么这个类就需要把这些接口的方法全部实现。
package lee;public interface Output
{// 接口里定义的成员变量只能是常量int MAX_CACHE_LINE = 50;// 接口里定义的普通方法只能是public的抽象方法void out();void getData(String msg);// 在接口中定义默认方法,需要使用default修饰default void print(String... msgs){for (String msg : msgs){System.out.println(msg);}}// 在接口中定义默认方法,需要使用default修饰default void test(){System.out.println("默认的test()方法");}// 在接口中定义类方法,需要使用static修饰static String staticTest(){return "接口里的类方法";}// 定义私有方法(java9)private void foo(){System.out.println("foo私有方法");}// 定义私有静态方法(java9)private static void bar(){System.out.println("bar私有静态方法");}
}
java 8特性:
默认方法:必须使用 default 修饰,该方法不能使用 static 修饰, 无论程序是否指定,默认方法总是使用 public 修饰。没有使用static 修饰,因此不能直接使用接口来调用默认方法,需要使用接口的实现类的实例来调用这些默认方法
类方法:类方法必须使用static 修饰 该方法不能使用 default 修饰,无论程序是否指定,类方法总是使用 public 修饰,类方法可以直接使用接口来调
Java9特性:
- Java9为接口增加了一种新的私有方法,其实私有方法的主要作用就是作为工具方法,为接 口中的 默认方法或类方法提供支持。私有方法可以拥有方法体,但私有方法不能使用 default 修饰。私有方法 可以使用 static 修饰,也就是说,私有方法既可是类方法,也可是实例方法
接口里的成员变量默认是使用 public static final 修饰的,因此即使另 个类处于不同包下,也可以 通过接口来访问接口里的成员变量。
public class OutputFieldTest
{public static void main(String[] args){// 访问另一个包中的Output接口的MAX_CACHE_LINESystem.out.println(Output.MAX_CACHE_LINE);// 下面语句将引起"为final变量赋值"的编译异常Output.MAX_CACHE_LINE = 20;// 使用接口来调用类方法System.out.println(Output.staticTest());}
}
接口继承
接口的继承和类继承不一样,接口完全支持多继承,即一 个接口可以有多个直接父接口。和类继承相似,子接口扩展某个父接口,将会获得父接口里定义的所有抽象方法、常量。
一个接口继承多个父接口时,多个父接口排在 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 implements InterfaceC {@Overridepublic void testA() {System.out.println("A");}@Overridepublic void testB() {System.out.println("B");}@Overridepublic void testC() {System.out.println("C");}public static void main(String[] args) {InterfaceC demo1 = new InterfaceExtendsTest();demo1.testA();demo1.testB();demo1.testC();System.out.println(InterfaceC.PROP_A);System.out.println(InterfaceC.PROP_B);System.out.println(InterfaceC.PROP_C);}}
InterfaceC 接口继承了 InterfaceA 和InterfaceB ,所以InterfaceC 中获得了它们的常量,因此在main()方法中能看到通过 InterfaceC 来访问 这些常量。
使用接口
接口不能用于创建实例,但接口可以引用于声明引用类型变量。当使用接口来声明引用类型变量时,这个引用类型变量必须引用到其实现类的对象。除此之外,接口的主要用途就是被实现类实现。
接口主要用途:
- 定义变量,也可用于进行强制类型转换
- 调用接口中定义的常量
- 被其他类实现
一个类可以实现 个或多个接口,继承使用 extends 关键字,实现则使用 implements 关键字。因为一个类可以实现多个接口,这也是 Java 为单继承灵活性不足所做的补充。
类实现接口格式
[修饰符] class 类名 extends 父类 implements 接口 接口 2. . .
{类体部分
}
- 实现接口与继承父类相似,可以获得所实现接口里定义的常量、方法(包括抽象 方法和默认方法)
- 让类实现接口需要类定义后增加 implements 部分,当需要实现多个接口时,多个接口之间以英文逗号( ,)隔开,一个类可以继承一个父类,并同时实现多个接口, implements 部分必须放在 extends 部分之后。
- 一个类实现了一个或多个接口之后 ,这个类须完全实现这些接口里所定义的全部抽象方法(也就 是重写这些抽象方法);否则,该类将保留从父接口那里继承到的抽象方法,该类也必须定义成抽象类
- 一个类实现某个接口时,该类将会获得接口中定义的常量 、方法。因此可以把实现接口理解为一种特殊的继承,相当于实现类继承了一个彻底抽象的类(相当于除默认方法外,所有方法都是抽象方法的类)。
- 如果一个类A继承了父类B,又重写了C接口,C接口中的抽象方法,可以在类A中进行重写,也可以在A的父类B中进行重写。
// 定义一个Product接口
interface Product
{int getProduceTime();
}
// 让Printer类实现Output和Product接口
public class Printer implements Output , Product
{private String[] printData= new String[MAX_CACHE_LINE];// 用以记录当前需打印的作业数private int dataNum = 0;public void out(){// 只要还有作业,继续打印while(dataNum > 0){System.out.println("打印机打印:" + printData[0]);// 把作业队列整体前移一位,并将剩下的作业数减1System.arraycopy(printData , 1, printData, 0, --dataNum);}}public void getData(String msg){if (dataNum >= MAX_CACHE_LINE){System.out.println("输出队列已满,添加失败");}else{// 把打印数据添加到队列里,已保存数据的数量加1。printData[dataNum++] = msg;}}public int getProduceTime(){return 45;}public static void main(String[] args){// 创建一个Printer对象,当成Output使用Output o = new Printer();o.getData("轻量级Java EE企业应用实战");o.getData("疯狂Java讲义");o.out();o.getData("疯狂Android讲义");o.getData("疯狂Ajax讲义");o.out();// 调用Output接口中定义的默认方法o.print("孙悟空" , "猪八戒" , "白骨精");o.test();// 创建一个Printer对象,当成Product使用Product p = new Printer();System.out.println(p.getProduceTime());// 所有接口类型的引用变量都可直接赋给Object类型的变量Object obj = p;}
}
注意:
实现接口方法时,必须使用 public 访问控制修饰符,因为接口里的方法都是 public 的,而子类(相当于实现类)重写父类方法时访问权限只能更大或者相等,所以实现类实现接口里的方法时只能使用 public 访问权限.
Product p = new Printer();System.out.println(p.getProduceTime());// 所有接口类型的引用变量都可直接赋给Object类型的变量Object obj = p;
接口不能显式继承任何类 但所有接口类型的引用变量都可以直接赋给 Object 类型的引用变量。 所以在上面程序中可以把 Product 类型的变量直接赋给 Object 型变 ,这 用向上转型来实现的, 因为编译器知道任何 Java 对象都必须是 Object 或其子类的实例 Product 类型的对象 不例 (它必须 Product 接口实现类的对象,该实现类肯定是 Object 的显式或隐式子类)。
package riviewjiekou;interface Native {void eat();default void drink() {System.out.println("喝");}static void move() {System.out.println("玩");};}
public class Animal implements Native {@Overridepublic void eat() {// TODO Auto-generated method stubSystem.out.println("我会吃");}public static void main(String[] args) {Native a = new Animal();a.drink(); 喝a.eat(); 我会吃Native.move(); 玩}}
- 如果编译时类型是接口,运行时类型是接口的实现类,那么这个变量只能调用接口中定义的方法,不能调用实现类中定义,接口中未定义的方法
- 如果我们想在接口的实现类中定义非接口或者父类中的方法,我们不能使用@Override关键字,否则会产生报错。
接口:
实现类
接口和抽象类
接口
接口作为系统与外界交互的窗口,接口体现的是一种规范。接口类似于整个系统的"总纲",接口不应该经常改变,一旦接口被改变,对整个系统甚至其他系统的影响将是辐射式的, 导致系统中大部分类都需要改写。
对于接口的实现者而言,接口 规定了实现者必须向外提供哪些服务(以方法的形式来提供),对于接口的调用者而 接口规定了调用者可 调用哪些服务,以及如何调用这些服务(就是如何来调用方法)。
抽象类
抽象类不一样,抽象类作为系统中多个子类的共同父类 它所体现的是一种模板式设计。 抽象类 作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品 这个中间产品己经实现了系统部分功能(那些己经提供实现的方法) ,但这个产品依然不能当成最终产品,必须有更进一步的完善 ,这一种完善可能有几种不同方法。
共同特征
接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。
接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法
差别
- 接口里只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现;抽 象类则完全可以包含普通方法。
- 接口里只能定义静态常 ,不能定义普通成员变 ;抽象类里则既可以定义普通成员变 ,也 可以定义静态常量。
- 接口里不包含构造器;抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而 是让其子类调用这些构造器来完成属于抽象类的初始化操作
- 接口里不能包含初始化块:但抽象类则完全可以包含初始化块
- 一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口,通过实现多个接口可以弥补 Java 单继承的不足
面向接口编程
- 简单工厂模式
- 命令模式
案例
【案例目标】
案例描述
饲养员在给动物喂食时,给不同的动物喂不同的食物,而且在每次喂食时,动物都会发出欢快的叫声。例如,给小狗喂骨头,小狗会汪汪叫;给小猫喂食,小猫会喵喵叫。
本案例要求编写一个程序模拟饲养员喂食动物的过程,案例要求如下:
输出内容:
饲养员给小狗喂骨头,小狗汪汪叫。
饲养员给小猫喂小鱼,小猫喵喵叫。
【案例分析】
在这个动物园里,涉及的对象有饲养员,各种不同动物以及各种不同的食物。这样很容易抽象出3个类Feeder、Animal和Food。假设只考虑猫类和狗类动物,则由Animal类派生出Cat类、Dog类、同样由Food类可以进一步派生出其子类Bone、Fish。因为他们之间存在这明显的is-a关系。
同样的,鱼是一种食物,但实际上,鱼也是一种动物,Cat类和Dog类继承了Animal的很多属性和方法,如果将Animal定义为接口,Animal中是不能定义成员变量和成员方法的,Food类中虽然也有变量但是数量比Animal少,所以我们考虑将Food定义为接口,此时可以说“鱼是一种动物,同时也是一种食物”。
abstract class TestAnimal {}abstract class Animal extends TestAnimal{// @Overridepublic int getAge() {return 0;}private String name;public Animal(String name) {this.name = name;}public abstract void shout();public abstract void eat(Food food);public String getName() {return name;}public void setName(String name) {this.name = name;}
}class Dog extends Animal {public Dog(String name) {super(name);}@Overridepublic void shout() {System.out.print ("汪汪汪~~~");}@Overridepublic void eat(Food food) {System.out.println(getName() + "正在啃着香喷喷的" + food.getName());}
}class Cat extends Animal{public Cat(String name) {super(name);}public void shout() {System.out.print("喵喵喵~~~");}public void eat(Food food) {System.out.println(getName() + "正在吃着香喷喷的"+food.getName());}
}
interface Food {String getName();int getAge();
}class Bone implements Food{@Overridepublic String getName() {return "骨头";}@Overridepublic int getAge() {return 0;}}class Fish extends Animal implements Food{public Fish(String name) {super(name);}@Overridepublic void shout() {}@Overridepublic void eat(Food food) {}}
class Feeder {private String name;public Feeder(String name) {this.name = name;}public void speak() {System.out.println("欢迎来到动物园!");System.out.println("我是饲养员 "+getName());}public void feed(Animal a, Food food) {a.eat(food);}public String getName() {return name;}public void setName(String name) {this.name = name;}
}
public class DongWuTest {public static void main(String[] args) {Feeder feeder = new Feeder("小华");feeder.speak();Dog dog = new Dog("小狗");dog.shout();Food food = new Bone();feeder.feed(dog, food);Cat cat = new Cat("小猫");cat.shout();food = new Fish("黄花鱼");feeder.feed(cat, food);}
}
6.内部类
大部分时候,类被定义一个独立的程序单元。在某些情况下, 类放在另一个类的内部 定义,这个定义在其他类内部的类就被称为内部类(有 的地方也叫 嵌套类),包含内部类的类也被称为外部类(有的地方也叫宿主类)
作用:
- 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。 假设需要创建 Cow 类, Cow 类需要组合 CowLeg 对象, CowLeg 类只有在 Cow 类里 才有效,离开了 Cow 类之后没有任 意义 在这种情况下 就可 CowLeg 定义成 Cow 的内部类,不允许其他类访问 CowLeg。
- 内部类成员可以直接访问外部类的私有数据,因为内部类被当成其外部类成员,同 个类的成 员之间可以互相访 但外部类不能访 问内 部类的实现细节 ,例如内部类的成员变量
- 匿名内部类适合用于创建那些仅需要一次使用的类。对于前面介绍的命令模式,当需要传入 一个Command 对象时,重新专门定义 PrintCommand 和 AddCommand 两个实现类可能没有太大的 意义,因为这两个实现类可能仅需要使用一次。 在这种情况下,使用匿名内部类将更方便。
- 从语法角度来看,定义内部类与定义外部类的语法大致相同,内部类除需要定义在其他类里面之外, 还存在如下两点区别
- 内部类比外部类可以多使用三个修饰符: private protected statÍc 一一外部类不可 以使用 修饰符
- 非静态内部类不能拥有静态成员
非静态内部类
定义内部类
public class OuterClass
{// 此处可以定义内部
}
大部分时候,内部类都被作为成员内部类定义,而不是作为局部内部类 。
成员内部类是一种与成员变量、方法、构造器和初始化块相似的类成员;局部内部类和匿名内部类则不是类成员。
成员内部类分为两种:静态内部类和非静态内部类,使用 static 修饰的成员内部类是静态内部类,没有使用 statÍc 修饰的成员内部类是非静态部类
注意:同一个java文件中定义的多个类不是内部类,是相互独立的类
// 下面 两个空类互相独立 没有谁是谁的 部类
class A{}
public class B{ }
上面两个类定义虽然写在同一个源文件中,但它们互相独立,没有谁是谁的内部类这种关系。内部一定是放在另一个类的类体部分(也就是类名后的花括号部分)定义。 因为内部类作为其外部类的成员,所以可以使用任意访问控制符如 private protected public 等
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;}// length、color的setter和getter方法public void setLength(double length){this.length = length;}public double getLength(){return this.length;}public void setColor(String color){this.color = color;}public String getColor(){return this.color;}// 非静态内部类的实例方法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$CowLeg.class ,前者是外部类 Cow的 class 文件 后者是内部类 CowLeg的 class 文件,即成员内部类(包括静态内部类、非静态内部类)的 class 文件总是这种形式 OuterClass$ InnerClass.class
在非静态内部类里可以直接访问外部类的 private 成员,上面程序中①号粗体代码行, 就是在 CowLeg 类的方法内直接访问其外部类的 private 实例变量 这是因为在非静态内部类对象里, 保存了一个它所寄生的外部类对象的引用(当调用非静态内部类的实例方法时,必须有一非静态内部 类实例,非静态内部类实例必须寄生在外部类实例里。
当在非静态内部类的方法内访问某个变量时,系统会从当前方法内查是否存在该变量,不存在则依次查内部类—外部类。如果查不到,则报错。
如果外部类成员变量、内部类成员变量与内部类里方法的局部变量同名,则可通过使用 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 acessOuterProp(){// 非静态内部类可以直接访问外部类的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();}
}
静态代码块访问类成员:报错
public class InnerNoStatic
{private class InnerClass{/*下面三个静态声明都将引发如下编译错误:非静态内部类不能有静态声明*/static{System.out.println("==========");}private static int inProp;private static void test(){}}
}
非静态内部类里不能有静态方法、静态成员变量、静态初始化块,所以上面三个静态声明都会引发错误。
注意:
非静态内部类里不可以有静态初始化块,但可以包含普通初始化块 非静态内部类普通初始化块的作用与外部类初始化块的作用完全相同。
静态内部类
如果使用 static 来修饰 个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象。因此使用 static 修饰的内部类被称为类内部类,有的地方也称为静态内部类。
注意:
static 关键字的作用是把类的成员变成类相关,而不是实例相关,即 static 修饰的成员整个类,而不属于单个对象。
外部类的上级程序单元是包,所以不可使用 static 饰。
内部类的上 级程序单元是外部类,使用 static 修饰可以将内部类变成外部类相关, 而不是外部类实例相关,因此 static 关键字不可修饰外部类,但可修饰内部类。
静态内部类可以包含静态成员,也可以包含非静态成员。
根据静态成员不能访问非静态成员的规则, 静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。即使是静态内部类的实例方法也不能访问外部类的实例成员,只能访问外部类的静态成员 。
public class StaticInnerClassTest
{private int prop1 = 5;private static int prop2 = 9;static class StaticInnerClass{// 静态内部类里可以包含静态成员private static int age;public void accessOuterProp(){// 下面代码出现错误:// 静态内部类无法访问外部类的实例变量
// System.out.println(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(prop1);// 上面代码出现错误,应改为如下形式:// 通过类名访问静态内部类的类成员System.out.println(StaticInnerClass.prop1);// System.out.println(prop2);// 上面代码出现错误,应改为如下形式:// 通过实例访问静态内部类的实例成员System.out.println(new StaticInnerClass().prop2);}
}
Java 还允许在接口里定义内部类,接口里定义的内部类默认使用 public static 修饰,也 就是说 接口 部类只能是静态内部类。 如果为接口 部类指定访问控制符,则只能指定 public 访问控制符 如果定义接口内部类时省略访问控制符,则该内部类默认是 public 访问控制权限。
接口里定义内部接口
使用内部类
定义类的主要作用就是定义变量、创建实例和作为父类被继承 定义内部类的主要作用也如此,但 使用内部类定义变量和创建实例则与外部类存在一些小小的差异。
内部类用法:
- 在外部类内部使用内部类
- 在外部类以外使用非静态内部类
- 在外部类以外使用静态内部类
在外部类内部使用内部类
- **从前面程序中可以看出,在外部类内部使用内部类时,与平常使用普通类没有太大的区别 。**一样可直接通过内部类类名来定义变量,通过 new 用内部类构造器来创建实例。
- 存在的个区别是:不要在外部类的静态成员(包括静态方法和静态初始化块)中使用非静态内部类,因为静态成员不能访问非静态成员。 在外部类内部定义内部类的子类与平常定义子类也没有太大的区别。
外部类以外使用非静态内部类
如果希望在外部类以外的地方访问内部类(包括静态和非静态两种),则内部类不能使用private访问控制权限,private修饰的内部类只能在外部类内部使用。对于使用其他访问控制符修饰的内部类,则能在访问控制符对应的访问权限内使用。
省略访问控制符的内部类,只能被与外部类处于同一个包中的其他类所访问。
使用protected修饰的内部类,可被与外部类处于同一个包中的其他类和外部类的子类所访问。
使用public修饰的内部类,可以在任何地方被访问。
在外部类以外的地方定义内部类(包括静态和非静态两种)变量的语法格式如下:
OuterClass.InnerClass varName
在外部类以外的地方使用内部类时,内部类完整的类名应该是 OuterClass. InnerClass 如果外部类有包名,则还应该增加包名前缀.
由于非静态内部类的对象必须寄生在外部类的对象里,因此创建非静态内部类对象之前 ,必须先创建其外部类对象。在外部类以外的地方创建非静态内部类实例的语法如下:
OuterInstance.new InnerConstructor() 外部类实体.new 内部类构造器
从上面语法格式可以看出,在外部类以外的地方创建非静态内部类实例必须使用外部类实例和 new 来调用非静态内部类的构造器。
class Out
{// 定义一个内部类,不使用访问控制符,// 即只有同一个包中其他类可访问该内部类class In{public In(String msg){System.out.println(msg);}}
}
public class CreateInnerInstance
{public static void main(String[] args){Out.In in = new Out().new In("测试信息");/*上面代码可改为如下三行代码:使用OutterClass.InnerClass的形式定义内部类变量Out.In in;创建外部类实例,非静态内部类实例将寄存在该实例中Out out = new Out();通过外部类实例和new来调用内部类构造器创建非静态内部类实例in = out.new In("测试信息");*/}
}
非静态内部类的构造器必须使用外部类对象来调用。
当创建一个子类时,子类构造器总会调用父类的构造器,因此在创建非静态内部类的子类时,必须 **保证让子类构造器可以调用非静态内部类的构造器,**调用非静态内部类的构造器时,必须存在一个外部类对象。
public class SubClass extends Out.In
{//显示定义SubClass的构造器public SubClass(Out out){//通过传入的Out对象显式调用In的构造器out.super("hello");}
}
非静态内部类In类的构造器必须使用外部类对象来调用,代码中 super 代表调用In类的构造器,而out则代表外部类对象(上面的Out、In两个类直接来自于前一个CreateInnerInstance.java)。
如果需要创建SubClass对象时,必须先创建一个Out对象。这是合理的,因为SubClass是非静态内部类In类的子类,非静态内部类In对象里必须有一个对Out对象的引用,其子类SubClass对象里也应该持有对Out对象的引用。当创建SubClass对象时传给该构造器的Out对象,就是SubClass对象里Out对象引用所指向的对象。
非静态内部类ln对象和SubClass对象都必须持有指向Outer对象的引用,区别是创建两种对象时传入Out对象的方式不同:
当创建非静态内部类In类的对象时,必须通过Outer对象来调用new关键字;
当创建SubClass类的对象时,必须使用Outer对象作为调用者来调用In类的构造器。
展示
public class CowLegChild extends Cow.CowLeg{//显示定义SubClass的构造器public CowLegChild(Cow cow){//通过传入的Out对象显式调用In的构造器cow.super(1.5,"绿");}public static void main(String[] args) {Cow cow = new Cow();Cow.CowLeg cowLeg = new CowLegChild(cow);System.out.println(cowLeg);System.out.println(cowLeg.getColor());System.out.println(cowLeg.getLength());}
}
package mianxiangduixiang_six_niebulei;public class Cow {private double weight;// 外部类的两个重载的构造器public Cow(){}public Cow(double weight){this.weight = weight;}// 定义一个非静态内部类public class CowLeg{// 非静态内部类的两个实例变量private double length;private String color;// 非静态内部类的两个重载的构造器public CowLeg(){}public CowLeg(double length , String color){this.length = length;this.color = color;}// length、color的setter和getter方法public void setLength(double length){this.length = length;}public double getLength(){return this.length;}public void setColor(String color){this.color = color;}public String getColor(){return this.color;}// 非静态内部类的实例方法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();}}
在外部类以外使用静态内部类
因为静态内部类是外部类类相关的,因此创建静态内部类对象时无须创建外部类对象。
在外部类以外的地方创建静态内部类实例的语法如下:new OuterClass. InnerConstructor ()
class StaticOut
{// 定义一个静态内部类,不使用访问控制符,// 即同一个包中其他类可访问该内部类static class StaticIn{public StaticIn(){System.out.println("静态内部类的构造器");}}
}
public class CreateStaticInnerInstance
{public static void main(String[] args){StaticOut.StaticIn in = new StaticOut.StaticIn();/*上面代码可改为如下两行代码:使用OutterClass.InnerClass的形式定义内部类变量StaticOut.StaticIn in;通过new来调用内部类构造器创建静态内部类实例in = new StaticOut.StaticIn();*/}
}
不管是静态 部类还是非静态内部类,它们声明变量的语法完全一样。区 别只是在 建内部类对象时, 静态内部类只需使用外部类即可调用构造器,而非静态内部类 须使用外部类对象来调用构造器
定义静态内部类子类
public class StaticSubClass extends StaticOut.StaticIn {}
局部内部类
如果一个内部类放在方法里定义,则这个内部类就是一个局部内部类。
局部类仅在该方法里有效。由于局部内部类不能在外部类的方法以外的地方使用,因此局部内部类 不能使用访问控制符和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 = 8;System.out.println("InnerSub对象的a和b实例变量是:"+ is.a + "," + is.b);}
}
7.Lambda表达式
Lambda 表达式 支持将代码块作为方法参数, Lambda 表达式允许使用更简洁的代码来创建只有一个抽象方法的接口(这 种接口被称为函数式接口)的实例。
interface Command {// 接口里定义的process()方法用于封装“处理行为”void process(int[] target);
}class ProcessArray {public void process(int[] target, Command cmd){cmd.process(target);}
}public class CommandTest {public static void main(String[] args) {ProcessArray pa = new ProcessArray();int[] array = { 3, -4, 6, 4 };// 处理数组,具体处理行为取决于匿名内部类pa.process(array, new Command() {public void process(int[] target) {int sum = 0;for (int tmp : target) {sum += tmp;}System.out.println("数组元素的总和是:" + sum);}});}}
ProcessArray 类的 process() 方法处理数组时,希望可以动态传入一段代码作为具体 的处理行为,因此程序创建了 个匿名内部类实例来封装处理行为 从上面代码可以看出,用于封装处理行为的关键就是实现程序中process的内部。但为了向 process()方法传入相应程序,不得不使用匿名内部类的语法来创建对象。
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);});}
}
代码精简,不需要值出重写的方法名字,也不需要给重写的方法的返回值类型,只要给出重写的方法括号以及括号里的参数列表即可。
当使用 Lambda 表达式代替匿名内部类创建对象时, Lambda 表达式的代码块将会代替实现抽象方法的方法体, Lambda 表达式就相当一个匿名方法。
Lambda 表达式的主要作用就是代替匿名内部类的烦琐语法。
Lambda的组成
- 形参列表
- 允许省略形参类型。
- 当只有一个参数时,圆括号和参数都可以省略。
- 箭头
- 代码块
- 如果代码块只有一句,可以省略花括号
- 代码块中只有一条语句 ,即使该表达式需要返回值,也可以省略 return 关键
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();// Lambda表达式的代码块只有一条语句,可以省略花括号。lq.eat(()-> System.out.println("苹果的味道不错!"));// Lambda表达式的形参列表只有一个形参,省略圆括号lq.drive(weather ->{System.out.println("今天天气是:" + weather);System.out.println("直升机飞行平稳");});// Lambda表达式的代码块只有一条语句,省略花括号// 代码块中只有一条语句,即使该表达式需要返回值,也可以省略return关键字。lq.test((a , b)->a + b);}
}
Lambda表达式与函数式接口
Lambda 表达式的类型,也被称为"目标类型( target type) ", Lambda 表达式的目标类型必须是"函数式接口( functional interface ) 。函数式接口代表只包含一个抽象方法的接口 。函数式接口可以包含多个默认方法、类方法,但只能声明一个抽象方法。
Lambda 表达式的结果就是被当成对象,因此程序中完全可以使用 Lambda 表达式进行赋值。
//Runnable 接口中只包含一个无参数的方法
// Lambda 表达式代表的匿名方法实现了 Runnable 接口中唯一的、无参数的方法
//因此下面的 Lambda 表达式创建了 Runnable 对象
Runnable r = () -> { for(int i = 0 ; i < 100 ; i ++){System.out.println() ; }
};
Runnable原码,Runnable是Java本身提供的一个函数式接口
@java.lang.FunctionalInterface
public abstract interface java.lang.Runnable {// Method descriptor #1 ()Vpublic abstract void run();}
Lambda 表达式实现的是匿名方法。因此它只能实现特定函数式接 口中的唯一方法。这意味着 Lambda 表达式有如下两个限制
- Lambda 表达式的目标类型必须是明确的函数式接口
- Lambda 表达式只能为函数式接口创建对象 Lambda 表达式只能实现一个方法 因此它只能为 只有一个抽象方法的接口(函数式接口)创建对象
测试:
Object obj = () -> { for(int i = 0 ; i < 100 ; i ++) {System.out.print1n();}
}
从该错误信息可以看出, Lambda 表达式的目标类型必须是明确的函数式接口。上面代码将 Lambda 表达式赋值给 Object 变量,编译器只能确定该 Lambda 表达式的类型为 Object ,Object 井不是函数式接口 因此上面代码报错。
为了保证Lambda表达式的目标类型是一个明确的函数式接口,可以有如下三种常见方式。
将Lambda表达式赋值给函数式接口类型的变量。
Runnable r = () -> { for(int i = 0 ; i < 100 ; i ++){System.out.println() ; } };
将Lambda表达式作为函数式接口类型的参数传给某个方法。
interface Eatable {void taste(); } public void eat(Eatable e) {System.out.println(e);e.taste();}
使用函数式接口对 Lambda 表达式进行强制类型转换。
Object obj = (Runnable)() -> { for(int i = 0 ; i < 100 ; i ++) {System.out.println();}}
注意:同样的 Lambda 表达式的目标类型完全可能是变化的一一唯一的要求是, Lambda 表达式实现的匿名方法与目标类型(函数式接口)中唯一的抽象方法有相同的形参列表
@FunctionalInterface
interface FkTest {void run();
}
上面的函数式接口中仅定义了一个不带参数的方法,因此前面强制转型为 Runnable的 Lambda 达式也可强转为 FkTest 类型一一因为 FkTest 接口中的唯一的抽象方法是不带参数的,而该 Lambda 达式也是不带参数的
FkTest a =()->{};Object obj = (FkTest)() -> { for(int i = 0 ; i < 100 ; i ++) {System.out.println();}}
Lambda 表达式的本质很简单,就是使用简洁的语法来创建函数式接口的实例——这种语法避免了匿名内部类的繁琐。
方法引用和构造器引用
如果 Lambda 表达式的代码块只有一条代码,程序就可以省略 Lambda 表达式中代码块的花括号。 不仅如此,如果 Lambda 表达式的代码块只有一条代码,还可以在代码块中使用方法 引用和构造器引用。
方法引用和构造器引用可以让 Lambda 表达式的代码块更加简洁 方法引用和构造器引用都需要使 用两个英文冒号 。
引用类方法
该函数式接口中包含 convert()抽象方法,该方法负责将 String 参数转换为Integer
@FunctionalInterface
interface Converter{Integer convert(String from);
}
// 下面代码使用Lambda表达式创建Converter对象
Converter converter1 = from -> Integer.valueOf(from);
//上面 Lambda 表达式的代码块只有一条语句,因此程序省略了该代码块的花括号;而且由于表达式
//所实现的 convert()方法需要返回值,因此 Lambda 表达式将会把这条代码的值作为返回值。Integer val = converter1.convert("99");System.out.println(val); // 输出整数99
程序调用 converter1 对象的 conver()方法将字符串转换为整数了。
上面代码调用 converter 对象的 conver()方法时一一由于 converterl 对象是 Lambda 表达式创建的, convert()方法执行体就是 Lambda 表达式的代码块部分,因此上面程序输出 99。
上面 Lambda 表达式的代码块只有一行调用类方法的代码,因此可以使用如下方法引用进行替换
//方法引用代替 Larnbda 表达式:引用类方法
//函数式接口中被实现方法的全部参数传给该类方法作为参数
Converter converterl = Integer::valueOf;
对于上面的类方法引用,也就是调用Integer 类的 valueOf()类方法来实现 Converter 函数式接口中唯一的抽象方法,当调用 Converter 接口中的唯一的抽象方法时,调用参数将会传给Integer 类的 valueOf()类方法。
代码汇总
@FunctionalInterface
interface Converter{Integer convert(String from);
}
// 下面代码使用Lambda表达式创建Converter对象
// Converter converter1 = from -> Integer.valueOf(from);
// // 方法引用代替Lambda表达式:引用类方法。
// // 函数式接口中被实现方法的全部参数传给该类方法作为参数。Converter converter1 = Integer::valueOf;Integer val = converter1.convert("99");System.out.println(val); // 输出整数99
引用特定对象的实例
//下面代码使用 Larnbda 表达式创建 Converter 对象
Converter converter2 = from - > "fkit.org".indexOf (from) ;
上面 Lambda 表达式的代码块只有一条语句,因此程序省略了该代码块的花括号。
而且由于表达式 所实现的 convert()方法需要返回值,因此 Lambda 表达式将会把这条代码的值作为返回值。
Integer value = converter2.convert("it");System.out.println(value); // 输出2
上面 Lambda 表达式的代码块只有一行调用 “fkit.org” 的indexOf() 实例方法的代码 因此可以使用下面方法 进行替换
进行替换
Converter converter2 = "fkit.org"::indexOf;Integer value = converter2.convert("it");System.out.println(value); // 输出2
代码汇总
@FunctionalInterface
interface Converter{Integer convert(String from);
}
// 下面代码使用Lambda表达式创建Converter对象
// Converter converter2 = from -> "fkit.org".indexOf(from);// 方法引用代替Lambda表达式:引用特定对象的实例方法。// 函数式接口中被实现方法的全部参数传给该方法作为参数。Converter converter2 = "fkit.org"::indexOf;Integer value = converter2.convert("it");System.out.println(value); // 输出2
对于上面的实例方法引用 ,也就是调用 “fkit.org” 对象的 indexOf()实例方法来实现 Conerter 函数式接口中唯一的抽象方法,当调用 Conerter 接口中的唯一的抽象方法时,调用参数将会传给"fkit. org" 对象的 indexOf() 实例方法
引用某类对象的实例方法
@FunctionalInterface
interface MyTest
{String test(String a , int b , int c);
}
该函数式接口中包含 test()抽象方法,该方法负责根据 String int int 三个参数生成一个 String 返回值
// 下面 码使用 Lambda 表达式 MyTest 对象
MyTest mt = (a , b , c) - > a.substring (b , c} ;
// 下面代码使用Lambda表达式创建MyTest对象MyTest mt = (a , b , c) -> a.substring(b , c);String str = mt.test("Java I Love you" , 2 , 9);System.out.println(str); // 输出:va I Lo
上面 Lambda 表达式的代码块只有 a.substring(b , c) ,因此使用如下方法引用进行替换
// 方法引用代替 Lambda 表达式: 用某类对象 方法
// 函数式接口中被实现方法的第一个参数作为调用者
// 后面 参数全部传给该方法作为参数MyTest mt = String ::substring; String str = mt.test("Java I Love you" , 2 , 9);System.out.println(str); // 输出:va I Lo
对于上面的实例方法引用,就是调用某 String 对象的 sub tring()实例方法来实现 MyTest 函数式接口中唯一的抽象方法。当调用 MyTest 接口中 的唯一象方法 ,第一个调用参数将作为substring() 方法的调用者,剩下的调用参数会 substring()实例方法的调用参数
代码汇总
// 下面代码使用Lambda表达式创建MyTest对象
// MyTest mt = (a , b , c) -> a.substring(b , c);// 方法引用代替Lambda表达式:引用某类对象的实例方法。// 函数式接口中被实现方法的第一个参数作为调用者,// 后面的参数全部传给该方法作为参数。MyTest mt = String::substring;String str = mt.test("Java I Love you" , 2 , 9);System.out.println(str); // 输出:va I Lo
引用构造器
@FunctionalInterface
interface YourTest
{JFrame win(String title);
}
该函数式接口中包含 win()抽象方法 该方法负责根据 String 参数生成一个JFrame 返回值。下 面代 码使用Lambda表达式来创建 YourTest 对象。
// 下面代码使用 Lambda 表达式创建 YourTest 对象
YourTest yt = (String a) - > new JFrame(a);
上面 Lambda 表达式的代码块只有一条语句,因此程序省略了该代码块的花括号;而且由于表达式 所实现的 win()方法需要返回值,因此 Lambda 表达式将会把这条代码的值作为返回值。接下来程序就可以调用 yt 对象的 win()方法了。
JFrame jf = yt.win("我的窗口");System.out.println(jf);
上面代码调用 yt 对象的 win() 方法时一一由于 yt 对象是 Lambda 表达式创建的,因此 win()方法执 行体就是 Lambda 表达式的代码块部分,即执行体就是执行 new JFrame(a); 语句,并将这条语句的值作为方法的返回值。
上面 Lambda 表达式的代码块只有 new JFrame(),因此可以使用如下构造器引用进行替换
YourTest yt = JFrame::new;JFrame jf = yt.win("我的窗口");System.out.println(jf);
对于上面的构造器引用,就是调用某个 JFrame 类的构造器来实 YourTest 函数式接口中唯一抽象方法,当调用 YourTest 接口中的唯一的抽象方法时,调用参数将会传给 JFrame 构造器。从上面程 序中可以看出,调用 YourTest 对象的 win()抽 象方法时,实际只传入了 String 类型的参数,这个 String 类型的参数会被传给 JFrame 构造器一一这就确定了是调用 JFrame 类的、带一个 String 参数的构造器。
代码汇总
// 下面代码使用Lambda表达式创建YourTest对象
// YourTest yt = (String a) -> new JFrame(a);// 构造器引用代替Lambda表达式。// 函数式接口中被实现方法的全部参数传给该构造器作为参数。YourTest yt = JFrame::new;JFrame jf = yt.win("我的窗口");System.out.println(jf);
Lambda表达式与其他方式对比
@FunctionalInterface
interface Converter {Integer convert(int value1, int value2);
// Integer convert(int value1, int value2);
}@FunctionalInterface
interface ConverterTwo {Integer convert(String value);
}@FunctionalInterface
interface ConverterThree {String convert(String a, int b, int c);
}@FunctionalInterface
interface ConverterFour {JFrame win(String title);
}class ConverterImpl implements Converter {@Overridepublic Integer convert(int value1, int value2) {return Integer.compare(value1, value2);}
}public class LessionLambdaTest {public static void main(String[] args) {//实践类操作。
// int a = 1;
// int b = 2;
// //方法一:通过类实现接口的形式,来调用接口中的方法。
// Converter converterImpl = new ConverterImpl();
// System.out.println(converterImpl.convert(a, b));
// //方法二:通过匿名内部类的形式来调用接口中的方法。
// Converter converterNiMing = new Converter() {// @Override
// public Integer convert(int value1, int value2) {// return Integer.compare(value1, value2);
// }
// };
// System.out.println(converterNiMing.convert(a, b));
// //方法三:使用Lambda表达式的方式来调用接口中的方法。
// Converter converter1 = (v1, v2) -> Integer.compare(v1, v2);
// System.out.println(converter1.convert(1, 2));
// //方法四:使用精简的Lambda的方式来调用接口中的方法。
// Converter converter2 = Integer::compare;
// System.out.println(converter2.convert(1, 2));//实践对象操作
// ConverterTwo converterTwo1 = from -> "fkit.org".indexOf (from) ;
// System.out.println(converterTwo1.convert("rg"));
// ConverterTwo converterTwo2 = "fkit.org"::indexOf;
// System.out.println(converterTwo2.convert("rg"));//实践对象的方法操作
// ConverterThree converterThree = (a , b , c) -> a.substring(b , c);
// String str = converterThree.convert("Java I Love you" , 2 , 9);
// System.out.println(str); // 输出:va I Lo//实践对象的构造方法操作ConverterFour converterFour = a -> new JFrame(a);ConverterFour converterFour1 = JFrame::new;ConverterFour converterFive = a -> {JFrame jFrame = new JFrame(a);jFrame.setVisible(true);jFrame.setSize(800, 900);return jFrame;};converterFive.win("zha");}
}
Lambda表达式与内部类的联系和区别
Lambda 表达式是匿名内部类的一种简化,因此它可以部分取代匿名内部类的作用, Lambda 表达式与匿名内部类存在如下相同点
- Lambda 表达式与匿名内部类一样,都可以直接访问 “effectively final” 的局部变量(不能修改变量),以及外部类的成员变量(包括实例变量和类变量)effectively final表示定义之后就没有更改过的变量
- 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 = "河南师范大学";public void test() {String book = "Java";//匿名内部类写法Displayable a = new Displayable() {public void display() {System.out.println("123456");};};System.out.println(a.add(1, 2));//函数式接口写法Displayable dis=()->{//book="js"; // 报错,函数式接口中,不能修改变量的值System.out.println("boo局部变量为:"+book);System.out.println("外部类的 age 实例变量为:"+age);System.out.println("外部类的 name 类变量为:"+name);//System.out.println(add(1,5)); // 报错,函数式接口中,不能调用接口中的默认方法};dis.display();//调用dis对象从接口中继承的add()方法System.out.println(dis.add(3, 5)); }public static void main(String[] args) {LambdaAndInner la = new LambdaAndInner();la.test();}}
与匿名内部类相似的是,由于 Lambda 表达式访问了 book 局部变量,因此该局部变量相当于有一个隐式的 final 修饰,因此同样不允许对 book 局部变量重新赋值。
当程序使用 Lambda 表达式创建了 Displayable 的对象之后,该对象不仅可调用接口中唯一的抽象 方法,也可调用接口中的默认方法,如上面程序中①号粗体字代码所示。
Lambda 表达式与匿名内部类主要存在如下区别:
- 匿名内部类可以为任意接口创建实例一一不管接口包含多少个抽象方法,只要匿名内部类实现 所有的抽象方法即可;但 Lambda 表达式只能为函数式接口创建实例。
- 匿名内部类可以为抽象类甚至普通类创建实例;但 Lambda 表达式只能为函数式接口创建实例。
- 匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法;但 Lambda 表达式的代 码块不允许调用接口中定义的默认方法。
Displayable dis=()->{//book="js"; // 报错,函数式接口中,不能修改变量的值System.out.println("boo局部变量为:"+book);System.out.println("外部类的 age 实例变量为:"+age);System.out.println("外部类的 name 类变量为:"+name);//System.out.println(add(1,5)); // 报错,函数式接口中,不能调用接口中的默认方法
虽然 Lambda 表达式的目标类型: Displayable中包含了 add ()方法,但 Lambda 表达式的代码块不允 许调用这个方法;如果将上面的 Lambda表达式改为匿名内部类的写法,当匿名内部类实现display()抽象方法时,则完全可以调用这个 add()方法
//匿名内部类写法Displayable a = new Displayable() {public void display() {System.out.println("123456");};};System.out.println(a.add(1, 2));
使用Lambda表达式调用Arrays的类方法
public class LambdaArrays
{public static void main(String[] args){String[] arr1 = new String[]{"java" , "fkava" , "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};// left代表数组中前一个所索引处的元素,计算第一个元素时,left为1// right代表数组中当前索引处的元素Arrays.parallelPrefix(arr2, (left, right)-> left * right);System.out.println(Arrays.toString(arr2));long[] arr3 = new long[5];// operand代表正在计算的元素索引Arrays.parallelSetAll(arr3 , operand -> operand * 5);System.out.println(Arrays.toString(arr3));//遍历数组Integer[] arr = {1,5,2,7};//dArrays.parallelSort(arr, (v1,v2) -> v2-v1);Arrays.asList(arr).stream().forEach(x -> System.out.println(x));Arrays.asList(arr).stream().forEach(System.out::println);}
}
详细查看API文档。
8.枚举类
某些情况下,一个类的对象是有限而且固定的,比如季节类,它只有四个对象,再比如行星类,目前只有八个对象 这种实例有限而且固定的类,在 Java 里被称为枚举类。
介绍
手动实现枚举类
直接使用简单的静态常量表示枚举。
public static final int SEASON_SPRING = 1;
public static final int SEASON_SUMMER = 2;
public static final int SEASON_FALL = 3 ;
public static final int SEASON_WINTER = 4;
优点:简介明了
缺点:
- 类型不安全:使用int定义,可以把季节当成一个int整数使用。进行加法运算时,程序运行异常但是不符合常理。
- 没有命名空间:当定义季节时,必须添加SEASON_前缀,否则极易和其他静态成员混交。
- 打印输出的意义不明确:输出SEASON_SPRING表示的值为1,不能表示春天。
早期定义枚举类
- 通过 private 将构造器隐藏起来
- 把这个类的所有可能实例都使用 public static final 修饰的类变量来保存
- 如果有必要,可以提供一些静态方法,允许其他程序根据特定参数来获取与之匹配的实例
- 使用枚举类可以使程序更加健壮,避免创建对象的随意性。
但通过定义类来实现枚举的代码量比较大,实现起来也比较麻烦。
public class Season
{// 把Season类定义成不可变的,将其成员变量也定义成final的private final String name;private final String desc;public static final Season SPRING= new Season("春天" , "趁春踏青");public static final Season SUMMER= new Season("夏天" , "夏日炎炎");public static final Season FALL= new Season("秋天" , "秋高气爽");public static final Season WINTER= new Season("冬天" , "围炉赏雪");public static Season getSeason(int seasonNum){switch(seasonNum){case 1 :return SPRING;case 2 :return SUMMER;case 3 :return FALL;case 4 :return WINTER;default :return null;}}// 将构造器定义成private访问权限private Season(String name , String desc){this.name = name;this.desc = desc;}// 只为name和desc提供getter方法public String getName(){return this.name;}public String getDesc(){return this.desc;}
}
入门
枚举类是一种特殊的类,它一样可以有自己的成员变量 、方法,可以实现一个或者多个接 口,也可以定义自己的构造器 。一 Java源文件中最多只能定义 public 访问权限的枚举类,且该 Java 源文件也必须和该枚举类的类名相同(和类一样)
枚举类与普通类的区别
- 枚举类可以实现一个或多个接口,使用 enum 定义的枚举类默认继承了 java.lang .Enum 类,而不是默认继承 Object 类,因此枚举类不能显式继承其他父类。其中 java.lang.Enum 类实现了 java.lang.Serializable, java.lang. Comparable 两个接口
- 使用 enum 定义、非抽象的枚举类默认会使用 final 修饰, 因此枚举类不能派生子类。
- 枚举类的构造器只能使用 private 访问控制符,如果省略了构造器的访问控制符,则默认使用 private 修饰
- 枚举类的所有实例必须在枚举类的第一行显式列出,否则这个枚举类永远都不能产生实例。列出这些实例时,系统会自动添加 public static final 修饰,无须程序员显式添加
**注意:**枚举类默认提供了 values()方法,该方法可以很方便地遍历所有的枚举值。
定义一个枚举类
publ enum SeasonEnum
{
//在第一行列出 个枚举实例
SPRING, SUMMER,FALL, WINTER;
}
对枚举类的操作
public class EnumTest {public void judge(SeasonEnum s) {// switch语句里的表达式可以是枚举值switch (s) {case SPRING:System.out.println("春暖花开,正好踏青");break;case SUMMER:System.out.println("夏日炎炎,适合游泳");break;case FALL:System.out.println("秋高气爽,进补及时");break;case WINTER:System.out.println("冬日雪飘,围炉赏雪");break;}}public static void main(String[] args) {// 枚举类默认有一个values方法,返回该枚举类的所有实例for (SeasonEnum s : SeasonEnum.values()) {System.out.println(s);}// 使用枚举实例时,可通过EnumClass.variable形式来访问new EnumTest().judge(SeasonEnum.SPRING);}
}
switch 控制表达式可以是任何枚举类型。不仅如此, switch 控制表达式使用枚举类型时, 后面 case 表达式中的值直接使用枚举值的名字,无须添加枚举类作为限定。
枚举类的成员,方法和构造器
枚举类也是一种类,只是它是一种比较特殊的类,因此它一样可以定义成员变量、方法和构造器。
定义一个枚举类Gender
public enum Gender {MALE,FEMALE; // 定义 public 修饰的实例变public String name ;
}
enum Gender {MALE,FEMALE;// 定义 public 修饰的实例变public String name ;
}public class GenderTest
{public static void main(String[] args){// 通过Enum的valueOf()方法来获取指定枚举类的枚举值Gender g = Enum.valueOf(Gender.class , "FEMALE");// 直接为枚举值的name实例变量赋值g.name = "女";// 直接访问枚举值的name实例变量System.out.println(g + "代表:" + g.name);System.out.println(Gender.FEMALE.name);Gender.MALE.name="男";System.out.println(Gender.MALE.name);}
}
上面程序使用Gender枚举类时与使用一个普通类没有太大的差别,差别只是产生Gender对象的方式不同,枚举类的实例只能是枚举值,而不是随意地通过new来创建枚举类对象。
正如前面提到的,Java应该把所有类设计成良好封装的类,所以不应该允许直接访问Gender类的name成员变量,而是应该通过方法来控制对name的访问。否则可能出现很混乱的情形,例如上面程序恰好设置了g.name = “女”,要是采用g.name=“男”,那程序就会非常混乱了,可能出现 FEMALE代表男的局面。
使用封装改进枚举类Gender
public enum Gender
{MALE,FEMALE;private String name;public void setName(String name){switch (this){case MALE:if (name.equals("男")){this.name = name;}else{System.out.println("参数错误");return;}break;case FEMALE:if (name.equals("女")){this.name = name;}else{System.out.println("参数错误");return;}break;}}public String getName(){return this.name;}
}
当试图将一个FEMALE枚举值的name变量设置为"男",系统将会提示参数错误。
实际上这种做法依然不够好,枚举类通常应该设计成不可变类,也就是说,它的成员变量值不应该允许改变,这样会更安全,而且代码更加简洁。因此建议将枚举类的成员变量都使用private final修饰。
如果将所有的成员变量都使用了final 修饰符来修饰,所以必须在构造器里为这些成员变量指定初始值(或者在定义成员变量时指定默认值,或者在初始化块中指定初始值,但这两种情况并不常见),因此应该为枚举类显式定义带参数的构造器。
一旦为枚举类显式定义了带参数的构造器,列出枚举值时就必须对应地传入参数。
使用构造方法改进枚举(常用)
public enum Gender
{// 此处的枚举值必须调用对应构造器来创建MALE("男"),FEMALE("女");private final String name;// 枚举类的构造器只能使用private修饰private Gender(String name){this.name = name;}public String getName(){return this.name;}
}
当为Gender 枚举类创建了一个 Gender(String name)构造器之后,在枚举类中列出枚举值时,实际上就是调用构造器创建枚举类对象,只是这里无须使用new关键字,也无须显式调用构造器。前面列出枚举值时无须传入参数,甚至无须使用括号,仅仅是因为前面的枚举类包含无参数的构造器。
MALE("男"),FEMALE("女");
//相当于
public static final Gender MALE = new Gender( 男) ;
public static final Gender FEMALE = new Gender( 女") ;
实现接口的枚举类
枚举类也可以实现一个或多个接口。与普通类实现一个或多个接口完全一样,枚举类实现一个或多个接口时,也需要实现该接口所包含的方法。
public interface GenderDesc
{void info();
}
public enum Gender implements GenderDesc
{// 此处的枚举值必须调用对应构造器来创建MALE("男")// 花括号部分实际上是一个类体部分{public void info(){System.out.println("这个枚举值代表男性");}},FEMALE("女"){public void info(){System.out.println("这个枚举值代表女性");}};// 其他部分与codes\06\6.9\best\Gender.java中的Gender类完全相同private final String name;// 枚举类的构造器只能使用private修饰private Gender(String name){this.name = name;}public String getName(){return this.name;}// 增加下面的info()方法,实现GenderDesc接口必须实现的方法public void info(){System.out.println("这是一个用于用于定义性别的枚举类");}
}
如果由枚举类来实现接口里的方法,则每个枚举值在调用该方法时都有相同的行为方式(因为方法体完全一样)。如果需要每个枚举值在调用该方法时呈现出不同的行为方式,则可以让每个枚举值分别来实现该方法,每个枚举值提供不同的实现方式,从而让不同的枚举值调用该方法时具有不同的行为方式。在下面的Gender枚举类中,不同的枚举值对info()方法的实现各不相同
MALE("男")// 花括号部分实际上是一个类体部分{public void info(){System.out.println("这个枚举值代表男性");}}
花括号部分实际上就是一个类体部分,在这种情况下,当创建MALE、FEMALE枚举值时,并不是直接创建Gender 枚举类的实例,而是相当于创建Gender 的匿名子类的实例。因为粗体字括号部分实际上是匿名内部类的类体部分,所以这个部分的代码语法与前面介绍的匿名内部类语法大致相似,只是它依然是枚举类的匿名内部子类。
问:枚举类为什么可以设置子类
包含抽象方法的枚举类
假设有一个Operation枚举类,它的4个枚举值PLUS, MINUS,TIMES, DIVIDE分别代表加、减、乘、除4种运算,该枚举类需要定义一个eval(方法来完成计算)。
从上面描述可以看出,Operation需要让PLUS、MINUS、TIMES、DIVIDE四个值对 eval()方法各有不同的实现。此时可考虑为Operation枚举类定义一个eval()抽象方法,然后让4个枚举值分别为eval()提供不同的实现。例如如下代码。
public enum Operation
{PLUS{public double eval(double x , double y){return x + y;}},MINUS{public double eval(double x , double y){return x - y;}},TIMES{public double eval(double x , double y){return x * y;}},DIVIDE{public double eval(double x , double y){return x / y;}};// 为枚举类定义一个抽象方法// 这个抽象方法由不同的枚举值提供不同的实现public abstract double eval(double x, double y);public static void main(String[] args){System.out.println(Operation.PLUS.eval(3, 4));System.out.println(Operation.MINUS.eval(5, 4));System.out.println(Operation.TIMES.eval(5, 4));System.out.println(Operation.DIVIDE.eval(5, 4));}
}
重要:
枚举类里定义抽象方法时不能使用abstract 关键字将枚举类定义成抽象类(因为系统自动会为它添加abstract关键字),但因为枚举类需要显式创建枚举值,而不是作为父类,所以定义每个枚举值时必须为抽象方法提供实现,否则将出现编译错误。
9.对象与垃圾回收
当程序创建对象、数组等引用类型实体时,系统都会在堆内存中为之分配一块内存区,对象就保存在这块内存区中,当这块内存不再被任何引用变量引用时,这块内存就变成垃圾,等待垃圾回收机制进行回收。
垃圾回收机制具有如下特征:
- 垃圾回收机制只负责回收堆内存中的对象,不会回收任何物理资源(例如数据库连接、网络IO等资源)。
- 程序无法精确控制垃圾回收的运行,垃圾回收会在合适的时候进行。当对象永久性地失去引用后,系统就会在合适的时候回收它所占的内存。
- 在垃圾回收机制回收任何对象之前,总会先调用它的finalize()方法,该方法可能使该对象重新复活(让一个引用变量重新引用该对象),从而导致垃圾回收机制取消回收。
对象在内存存在的状态
当一个对象在堆内存中运行时,根据它被引用变量所引用的状态,可以把它所处的状态分成如下三种。
- 可达状态:当一个对象被创建后,若有一个以上的引用变量引用它,则这个对象在程序中处于可达状态,程序可通过引用变量来调用该对象的实例变量和方法。
- 可恢复状态:如果程序中某个对象不再有任何引用变量引用它,它就进入了可恢复状态。在这种状态下,系统的垃圾回收机制准备回收该对象所占用的内存,在回收该对象之前,系统会调用所有可恢复状态对象的 finalize()方法进行资源清理。如果系统在调用finalize()方法时重新让一个引用变量引用该对象,则这个对象会再次变为可达状态;否则该对象将进入不可达状态。
- 不可达状态:当对象与所有引用变量的关联都被切断,且系统已经调用所有对象的 finalize()方法后依然没有使该对象变成可达状态,那么这个对象将永久性地失去引用,最后变成不可达状态。只有当一个对象处于不可达状态时,系统才会真正回收该对象所占有的资源。
public class StatusTranfer
{public static void test(){String a = new String("轻量级Java EE企业应用实战"); //①a = new String("疯狂Java讲义"); //②}public static void main(String[] args){test(); //③}
}
当程序执行test方法的①代码时,代码定义了一个a变量,并让该变量指向"轻量级JavaEE企业应用实战"字符串,该代码执行结束后,"轻量级JavaEE 企业应用实战"字符串对象处于可达状态。
当程序执行了test方法的②代码后,代码再次创建了"疯狂Java讲义"字符串对象,并让 a变量指向该对象。此时,"轻量级Java EE企业应用实战"字符串对象处于可恢复状态,而"疯狂Java讲义"字符串对象处于可达状态。
一个对象可以被一个方法的局部变量引用,也可以被其他类的类变量引用,或被其他对象的实例变量引用。
当某个对象被其他类的类变量引用时,只有该类被销毁后,该对象才会进入可恢复状态;
当某个对象被其他对象的实例变量引用时,只有当该对象被销毁后,该对象才会进入可恢复状态。
强制垃圾回收
当一个对象失去引用后,系统何时调用它的finalize()方法对它进行资源清理,何时它会变成不可达状态,系统何时回收它所占有的内存,对于程序完全透明。
程序只能控制一个对象何时不再被任何引用变量引用,绝不能控制它何时被回收。
程序无法精确控制Java垃圾回收的时机,但依然可以强制系统进行垃圾回收——这种强制只是通知系统进行垃圾回收,但系统是否进行垃圾回收依然不确定。大部分时候,程序强制系统垃圾回收后总会有一些效果。强制系统垃圾回收有如下两种方式。
调用System类的gc()静态方法:System.gc()。
调用Runtime对象的gc()实例方法: Runtime.getRuntime().gc()。
不使用gc测试
public class GcTest
{public static void main(String[] args){for (int i = 0 ; i < 4; i++){new GcTest();}}public void finalize(){System.out.println("系统正在清理GcTest对象的资源...");}
}
创建了四个对象,对象创建之后立即进入可恢复状态,等待系统回收,但是一直到系统回收,系统仍不回收资源。
编译、运行上面程序,看不到任何输出,可见直到系统退出,系统都不曾调用GcTest对象的 finalize()方法。
使用gc强制进行垃圾回收
public class GcTest
{public static void main(String[] args){for (int i = 0 ; i < 4; i++){new GcTest();// 下面两行代码的作用完全相同,强制系统进行垃圾回收// System.gc();Runtime.getRuntime().gc();}}public void finalize(){System.out.println("系统正在清理GcTest对象的资源...");}
}
java -verbose:gc GcTest //测试命令无效
程序强制垃圾回收会产生一定效果,但这种强制只是建议系统立即进行垃圾回收,系统完全有可能并不立即进行垃圾回收,垃圾回收机制也不会对程序的建议完全置之不理;垃圾回收机制会在收到通知后,尽快进行垃圾回收。
扩展学习
- finalize方法
- 对象的软,弱,虚引用
- 修饰符适用范围(native关键字)
- Jar包的使用
综合练习
学生管理系统
- 运行系统:
- 提示:欢迎来到学生管理系统
- 系统列表展示
- (1)查看所有学生 封装方法
- (2)添加学生 封装方法
- (3)删除学生 封装方法
- (4)修改学生 封装方法
- (5)退出系统 封装方法
- 操作系统:
- 进入系统后,选择菜单进行操作(输入1表示查看所有学生,2表示添加学生…依次类推)附:
- 查看所有学生:
- 没有学生:
- 提示:不好意思,目前没有学生信息可供查询,请回去重新选择你的操作
- 有:展示所有学生的基本信息(toString)
- 没有学生:
- 添加学生
- 添加学生
- 提示:请输入学号
- 进行学号判断
- 已有该学号:提示:你输入的学号已经被占用,请重新输入
- 否则:继续录入其他信息
- 进行学号判断
- 提示:请输入姓名
- 提示:请输入年龄
- 提示:请输入居住地
- 附:每次提示都是在上一次提示的完成的基础上操作
- 提示:请输入学号
- 添加学生
- 删除学生
- 提示:选择要删除学生的学号
- 存在:删除成功
- 不存在:删除失败。并提示该学生不存在,重新回到列表页。
- 提示:选择要删除学生的学号
- 修改学生
- 输入学号
- 存在:依次修改添加学生的基本信息
- 不存在:输入不存在,请重新填写
- 输入学号
- 退出系统:退出系统后,提示退出成功。
- 查看所有学生:
- 进入系统后,选择菜单进行操作(输入1表示查看所有学生,2表示添加学生…依次类推)附:
学生类
public class Student {//学号private String id;//姓名private String name;//年龄private String age;//居住地private String address;public String getId() {return id;}public void setId(String id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getAge() {return age;}public void setAge(String age) {this.age = age;}public String getAddress() {return address;}public void setAddress(String address) {this.address = address;}
}
系统
public class StudentSystem {public static void main(String[] args) {//创建集合对象,用于存储学生数据ArrayList<Student> array = new ArrayList<Student>();//为了让程序能够回到这里来,我们使用循环while(true) {//这是学生管理系统的主界面System.out.println("--------欢迎来到学生管理系统--------");System.out.println("1 查看所有学生");System.out.println("2 添加学生");System.out.println("3 删除学生");System.out.println("4 修改学生");System.out.println("5 退出");System.out.println("请输入你的选择:");//创建键盘录入对象Scanner sc = new Scanner(System.in);String choiceString = sc.nextLine();//用switch语句实现选择switch(choiceString) {case "1"://查看所有学生findAllStudent(array);break;case "2"://添加学生addStudent(array);break;case "3"://删除学生deleteStudent(array);break;case "4"://修改学生updateStudent(array);break;case "5"://退出//System.out.println("谢谢你的使用");//break;default:System.out.println("谢谢你的使用");System.exit(0); //JVM退出break;}}}//查看所有学生public static void findAllStudent(ArrayList<Student> array) {//首先来判断集合中是否有数据,如果没有数据,就给出提示,并让该方法不继续往下执行if(array.size() == 0) {System.out.println("不好意思,目前没有学生信息可供查询,请回去重新选择你的操作");return;}//\t 其实就是一个tab键的位置System.out.println("学号\t姓名\t年龄\t居住地");for(int x=0; x<array.size(); x++) {Student s = array.get(x);System.out.println(s.getId()+"\t"+s.getName()+"\t"+s.getAge()+"\t"+s.getAddress());}}//添加学生public static void addStudent(ArrayList<Student> array) {//创建键盘录入对象Scanner sc = new Scanner(System.in);//为了让id能够被访问到,我们就把id定义在了循环的外面String id;//为了让代码能够回到这里,用循环while(true) {System.out.println("请输入学生学号:");//String id = sc.nextLine();id = sc.nextLine();//判断学号有没有被人占用//定义标记boolean flag = false;//遍历集合,得到每一个学生for(int x=0; x<array.size(); x++) {Student s = array.get(x);//获取该学生的学号,和键盘录入的学号进行比较if(s.getId().equals(id)) {flag = true; //说明学号被占用了break;}}if(flag) {System.out.println("你输入的学号已经被占用,请重新输入");}else {break; //结束循环}}System.out.println("请输入学生姓名:");String name = sc.nextLine();System.out.println("请输入学生年龄:");String age = sc.nextLine();System.out.println("请输入学生居住地:");String address = sc.nextLine();//创建学生对象Student s = new Student();s.setId(id);s.setName(name);s.setAge(age);s.setAddress(address);//把学生对象作为元素添加到集合array.add(s);//给出提示System.out.println("添加学生成功");}//删除学生public static void deleteStudent(ArrayList<Student> array) {//删除学生的思路:键盘录入一个学号,到集合中去查找,看是否有学生使用的是该学号,如果有就删除该学生//创建键盘录入对象Scanner sc = new Scanner(System.in);System.out.println("请输入你要删除的学生的学号:");String id = sc.nextLine();//我们必须给出学号不存在的时候的提示//定义一个索引int index = -1;//遍历集合for(int x=0; x<array.size(); x++) {//获取到每一个学生对象Student s = array.get(x);//拿这个学生对象的学号和键盘录入的学号进行比较if(s.getId().equals(id)) {index = x;break;}}if(index == -1) {System.out.println("不好意思,你要删除的学号对应的学生信息不存在,请回去 重新你的选择");}else {array.remove(index);System.out.println("删除学生成功");}}//修改学生public static void updateStudent(ArrayList<Student> array) {//修改学生的思路:键盘录入一个学号,到集合中去查找,看是否有学生使用的是该学号,如果有就修改该学生//创建键盘录入对象Scanner sc = new Scanner(System.in);System.out.println("请输入你要修改的学生的学号:");String id = sc.nextLine();//定义一个索引int index = -1;//遍历集合for(int x=0; x<array.size(); x++) {//获取每一个学生对象Student s = array.get(x);//拿学生对象的学号和键盘录入的学号进行比较if(s.getId().equals(id)) {index = x;break;}}if(index == -1) {System.out.println("不好意思,你要修改的学号对应的学生信息不存在,请回去 重新你的选择");}else {System.out.println("请输入学生新姓名:");String name = sc.nextLine();System.out.println("请输入学生新年龄:");String age = sc.nextLine();System.out.println("请输入学生新居住地:");String address = sc.nextLine();//创建学生对象Student s = new Student();s.setId(id);s.setName(name);s.setAge(age);s.setAddress(address);//修改集合中的学生对象array.set(index, s);//给出提示System.out.println("修改学生成功");}}
}
面向对象(下)——Java教案(五)相关推荐
- 五年级下计算机教案,五年级下册信息技术教案
中学信息技术课程的开展是为了提升学生们对信息技术学科的了解,下面是小编想跟大家分享的中学信息技术教案,欢迎大家浏览.一.指导思想与任务目标:初中阶段信息技术课程,使学生初步具备获取信息.传输信息.处理 ...
- python编程求导数_面向对象编程 —— java实现函数求导
首先声明一点,本文主要介绍的是面向对象(OO)的思想,顺便谈下函数式编程,而不是教你如何准确地.科学地用java求出函数在一点的导数. 一.引子 defd(f) :defcalc(x) : dx= 0 ...
- 尚学堂Java第五章所有题目
尚学堂Java第五章所有题目 一.选择题 二.简答题 三.编码题 编码题一 编码题二 编码题三 这次的作业有一部分理解的可能不是很透彻,可能不是最优的方法,希望师傅们能给刚入门的我一点建议! 一.选择 ...
- Java第五课. 类对象
Java第五课. 类&对象 回顾 1. 冒泡:概念,实现:2个for循环分别起的作用 2. 二维数组可以理解成表格:有行有列,行当成一维数组,列当成一维数组中的元素,行有下标(几个一维数组), ...
- java游戏开发毕业论文_基于JAVA的五子游戏的开发设计毕业论文
基于JAVA的五子游戏的开发设计毕业论文 毕业论文基于 JAVA 的五子游戏的开发设计山东凯文科技职业学院摘 要人工智能是一门正在迅速发展的新兴的,综合性很强的交叉科学.它与生物工程.空间技术一起被并 ...
- java怎么监听多个组件,java web(五):java web三大组件之另外两个和八大监听器
java的三大组件指Servlet.Filter.Listener.八大监听器指八个接口.前面介绍了Servlet,现在介绍一下Filter拦截器以及拦截地址的设置, Listener监听那些事件. ...
- 对象属性结构赋值_(六)面向对象-下
(六)面向对象-下 关键字:static static 可以用来修饰的结构:主要用来修饰类的内部结构 属性.方法.代码块.内部类 static修饰属性:静态变量(类变量 属性,是否使用static修饰 ...
- 20165310 Java实验五《网络编程与安全》
20165310 Java实验五<网络编程与安全> 任务一 题目:①编写MyBC.java实现中缀表达式转后缀表达式的功能:②编写MyDC.java实现从上面功能中获取的表达式中实现后缀表 ...
- 【Java】《面向对象程序设计——Java语言》Castle代码修改整理
前言 最近闲来无事刷刷MOOC,找到以前看的浙大翁凯老师的<面向对象程序设计--Java语言>课程,重新过一遍仍觉受益颇深. 其中有一个Castle的例子,思路很Nice但代码很烂,翁凯老 ...
- linux java 工具_Linux运维知识之linux下java版本管理工具jenv使用介绍
本文主要向大家介绍了Linux运维知识之linux下java版本管理工具jenv使用介绍,通过具体的内容向大家展现,希望对大家学习Linux运维知识有所帮助. 不同的项目使用的java版本不同,每次切 ...
最新文章
- Spring-cloud-eureka-robbin-hystrix-feign
- 如何成为领袖? 学习任正非小沃森郭士纳
- 004_SpringMVC分层配置文件
- C语言 NULL与0 对应的地址
- adb install 和adb uninstall
- 传输层协议TCP和UDP的区别
- 646. Maximum Length of Pair Chain 最长的链条长度
- iOS项目工程添加.a文件遇到的Dsymutil Error
- 傲腾机器Linux,【收藏】傲腾装机必看!
- UA PHYS515A 电磁理论IV 时变电磁场理论1 含时的麦克斯韦方程
- 计算机表格折线图添加图例,如何将Excel表格中插入的折线图中的圆圈设置为方块或三角形...
- explain mysql_mysql explain详解
- DFI Update的原理与实现
- 日本艺伎的拍照姿势,竟源于孔子?看完真的涨知识···
- bzoj 3039 玉蟾宫 单调栈
- 首届华为开发者大赛沙龙牵手大连-与开发者共话赛事
- RS485,uart串口加瑞芬68协议
- 2019年‘泰迪杯’数据分析职业技能大赛A题——个人代码分享
- 语法制导翻译和中间代码生成
- [特别篇] 评中国药学家距诺贝尔奖一步之遥