第6章 面向对象(下)
6.1 Java8 增强的包装类
Java8 包含了8种基本数据类型,这8种基本数据类型不支持面向对象,也不具有对象的特性。
为了解决8种基本数据类型不能当做Object类型变量使用的问题,Java提供了包装类的概念,为8种基本数据类型定义了相应的引用类。
@int 对应Integer char 对应Character ,其他6种均为首字母大写。
JDK1.5提供了自动装箱和自动拆箱功能,可以将基本数据类型直接赋值给对应的包装类(或Object),包装类也可以直接赋值给对应的基本数据类型。
public class AutoBoxingUnboxing { public static void main(String[] args){ Integer inObj = 5; Object boolObj = true; int it = inObj; if(boolObj instanceof Boolean){ boolean b = (Boolean)boolObj; System.out.println(b); } } }
除此之外,包装类还可以实现基本类型和字符串的转换。
将字符串转换成基本数据类型有两种方法:
1、使用包装类提供的 parseXxx(String s) 静态方法
2、使用包装类提供的 Xxx(String s)构造器
Sting提供了多个重载的value()方法,用于将基本类型转换成字符串。
public class Primitive { public static void main(String[] args){ String intStr = "12345"; int t1 = Integer.parseInt(intStr); int t2 = new Integer(intStr); System.out.println(t2); String str = String.valueOf(t2); System.out.println(str); } }
@将基本类型转换成字符串,还有一种更简单的方法:和””拼接 :String str = 5+ “”;
包装类可以直接和数值比较:
Integer a = new Integer(6)
if(a>5){
…}
但两个包装类比较时,只有两个包装类指向同一个对象时才返回true。
@-128 ~127 的整数被放入一个叫cache的数组缓存起来,以后用到-128~127的整数就引用cache数组中的元素。
Java7 增强了包装类,所有包装类都有一个静态的compare(Xxx val,Xxx val2);
这样就可以比较基本类型值的大小。
Java8为整型包装类提供了无符号运算的方法。
6.2 处理对象
Java对象都是Object类的实例,可以直接调用Object中的方法。
6.2.1 打印对象和toString方法
使用println()打印对象 输出的是对象.toString()的返回值。
Object提供的toString() 返回 类目+@+hashCode 值,很多时候并不是用户想要的描述。
为了返回想要的描述,可以重写toString()方法。
public String toString(){
return …;}
通常这样重写toString()
public String toString()
{
return “Apple[color=”+color+”,weight=”+weight+”]”;
返回: 类名[field1=值1,field2=值2,…]
6.2.2 == 和 equals方法
==: 判断基本类型时,值相等就返回true;判断引用,指向同一个对象时返回true。
@JVM常量池保证相同字符串直接量只有一个。 而使用 new String()创建的字符串对象是运行时创建的,不会放入常量池。
equals:equals()方法是Object提供的一个实例方法,效果和==没区别。 如果希望采用自定义的相等标准,则可以重写equals方法。
@String重写了equals()方法,当字符串值相等时就认为两个字符串相等。
重写
public Boolean equals(Object obj){…}
6.3 类成员
6.3.1 理解类成员
static 修饰的成员就是类成员,类成员属于整个类,而不属于单个对象。
当系统第一次准备使用类时,系统会位为类变量分配内存。
@对于static而言,有一条重要的规则,类成员不能访问实例成员。因为会出现类成员已经初始化,而实例成员还不曾初始化。
6.3.2 单例(Singleton)类
如果一个类始终只能创建一个实例,则这个类被称为单例类。
创建单例类的原则:
1、为了避免其他类自由创建该类,应该将构造器隐藏起来(private)
2、提供一个public 方法作为类的访问点,用于创建类的对象,且该方法必须是static。
3、该类还必须用一个成员变量缓存已经创建的对象,确保只创建一个对象。因此该成员变量要被上面的静态方法访问,也需要是static。
class Singleton { private static Singleton instance; private Singleton(){} public static Singleton getInstance() { if(instance==null){ instance = new Singleton(); } return instance; } } public class SingletonTest { public static void main(String[] args){ Singleton s1 = Singleton.getInstance(); Singleton s2 = Singleton.getInstance(); System.out.println(s1==s2); } }
6.4 final 修饰符
final修饰变量时,表示该变量一旦获得初始值就不可改变。
6.4.1 final成员变量
成员变量是随类或对象初始化的。对于final变量而言,如果没有在定义时初始化,也没有在初始化块、构造器中指定初始值,这些变量就一直是默认的值,也就失去了意义。
@Java规定,final修饰的成员变量必须有程序员显式地指定初始值。
public class FinalVarTest { final int a = 6; final Stirng str; final int c; final static double d; final char ch; {//初始化块,给实例变量赋值 str ="Hello"; // a = 9 ,错误,不能重复赋值 } static{//静态初始化块,可以给类变量指定初始值 d = 5.6; } public FinalVarTest() { c = 5; } public void changeFinal(){ //普通方法不能给final变量赋值 } }
6.4.2 final 局部变量
final修饰的局部变量若在定义时没有指定默认值,则可以在后面进行赋值。但只能一次。
final修饰的形参不能被赋值。
6.4.3 final修饰基本类型变量和引用类型变量的区别
final修饰基本类型变量,不能对基本类型变量重新赋值,因此基本类型变量不会改变。
但是对引用类型而言,它保存的仅仅是一个引用,final只保证这个引用类型变量的地址不会变,即一直引用同一个对象,但引用的对象(属性等)可以改变。
6.4.4 可执行“宏替换”的final变量
一个变量满足3个条件时,就相当于一个直接量:
1、final修饰
2、在定义时指定初始值
3、该初始值在编译时就可以确定下来。
6.4.5 final方法
final修饰的方法不可被重写,如果不希望子类重写某些方法,可以用final修饰。
6.4.6 final类
final 类不可以有子类
6.4.7 不可变类
创建该类的实例后,该实例的实例变量是不可改变的。
例如Java提供的String类
如果要创建不可变类,可遵循以下规则:
1、使用private和final修饰该类的成员变量
2、提供带参数构造器,根据传入参数初始化成员变量。
3、进提供getter()方法,不提供setter()方法。
4、如果有必要,重写hashCode()和equals()方法。
@设计不可变类时,尤其要注意其引用类型的成员变量,如果引用类型的的成员变量是可变的,就必须采取措施保证该成员变量不会被修改。
如:
class Name { private String firstName; private String lastName; //构造器 //setter(),getter() } 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()); //firstname被修改
为了保持Person对象的不变性,必须让程序无法访问name,程序就无法用name的可变现来改变Person了。
public class Person { private final Name name; public Person(Name name){ this.name = New Name(name,getFirstname(),name.getLastName());} //设置name为临时创建的Name public Name getName() {return New Name(name,getFirstname(),name.getLastName());} //返回匿名对象
6.4.8 缓存实例的不可变性
不可变类的实例状态不可改变,可以方便地被多个对象共享。
如果经常需要使用相同的不可变类实例,则应该考虑缓存这种不可变类的实例。
6.5 抽象类
抽象类主要用于规定子类应该实现的方法,但无法知道子类如何具体实现这些方法。
比如一个Shape类,这个类应该提供一个计算周长的方法calPerimeter(),但不同Shape子类计算周长的方法不同。
这时候就可以将这个方法定义为抽象方法,不提供实现细节。
6.5.1 抽象方法和抽象类
关键字 abstract , 有抽象方法的类只能被定义为抽象类,抽象类里可以没有抽象方法。
规则:
1、抽象方法不能有方法体,例,public abstract void test(); //注意后面直接没有方法体,而不是空方法体
2、抽象类不能被实例化
3、抽象类的构造器不用于创建实例,主要用于被子类调用。
定义抽象方法只需要在普通方法前加abstract,并且去掉方法体,在方法后加分号。
定义抽象类只需要在普通类前加abstract。
一个普通类继承抽象类后,必须实现抽象类的所有抽象方法。
利用抽象类和抽象方法的优势,可以更好发挥多态的优势。
6.5.2 抽象类的作用
抽象类体现的是一种模板模式的设计,抽象类作为多个子类的通用模板。
6.6 Java 8 改进的接口
接口(interface)可以看成一种更特殊的抽象类,接口里不能包含普通方法,都是抽象方法。
Java8 对接口进行改进,允许在接口中定义默认方法,默认方法可以提供方法实现。
6.6.1 接口的概念
接口是一种规范。
6.6.2 Java 8中接口的定义
[修饰符] interface 接口名 extends 父接口1,父接口2,…
{
常量定义…
抽象方法定义…
内部类、接口、枚举定义…
默认方法或类方法定义…
}
修饰符可以是public 或默认(包权限)
由于接口定义的是一种规范,因此接口里不含构造器和初始化定义。
对于接口中定义的静态常量而言,它们与接口相关,系统会自动为其增加static final修饰符。
int MAX_SIZE = 50;
public static final int MAX_SIZE = 50;
两句是等价的。
接口中的普通方法(除了默认方法)总是public abstract, 系统也会自动生成。
定义默认方法需要 default修饰。且不能用static修饰。
定义类方法,必须用static修饰,且总是public。类方法可以直接用接口调用。
@一个源文件只能有一个public接口,且与文件名相同。
6.6.3 接口的继承
接口支持多继承
interface interfaceC extends interface A, interface B
6.6.4 使用接口
接口作用:
1、定义变量
2、调用接口中常量
3、被类实现。
一个类可以实现(implements)多个接口。
[修饰符] class 类名 extends 父类 implements 接口1,接口2…
{…}
一个类实现接口后,必须实现接口中定义的全部抽象方法。
6.6.5 接口和抽象类
6.6.6 面向接口编程
核心是降低耦合,提高扩展性和可维护性
1、简单工厂模式
//接口Output public interface Output { int MAX_CACHE_LINE = 50; void out(); void getData(String msg); default void print(String...msgs){ for(String msg:msgs){ System.out.println(msg); } } default void test(){ System.out.println("默认的test()方法"); } static String staticTest(){ return "接口中的类方法"; } } interface Product { int getProduceTime(); } //实现Output和Product接口的类Printer 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]); System.arraycopy(printData, 1, printData, 0, --dataNum); //作业队列整体前移 } } public void getData(String msg){ if(dataNum>=MAX_CACHE_LINE){ System.out.println("队列满,添加失败"); } else{ printData[dataNum++] = msg; } } public int getProduceTime(){ return 45; } public static void main(String[] args){ Output o = new Printer(); o.getData("java ee"); o.getData("java SE"); o.out(); o.getData("java讲义"); o.out(); o.print("孙悟空","猪八戒"); o.test(); Product p = new Printer(); System.out.println(p.getProduceTime()); } } //Computer类,里面包含一个实现Output接口的类out public class Computer {
private Output out; //out 使得Computer和实际的Output类型对象分离,out可以是一个Printer,也可以是其他实现Output接口的xxxxPrinter。
public Computer(Output out){ this.out = out; } public void keyIn(String msg){ out.getData(msg); } public void print(){ out.out(); } } //工厂类,返回一个Output实现类的实例。 public class OutputFactory { public Output getOutput(){ return new Printer(); // 返回一个Output类,具体返回什么都可以在这个方法里定义。 } public static void main(String[] args){ OutputFactory of = new OutputFactory(); Computer c = new Computer(of.getOutput()); c.keyIn("Java EE"); c.keyIn("疯狂java"); c.print(); } }
如果Printer更换成BetterPrinter。即使很多类都使用了Printer对象,也只要修改OutputFactory的getOutput()方法生成BetterPrinter。无需改动Computer。
2、命令模式 :将“处理行为”当做参数传入方法。
public interface Command { void process(int[] target); } public 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 [] target = {1,3,5,5}; pa.process(target, new PrintCommand()); //……将处理行为传入方法 System.out.println("-------------"); pa.process(target, new AddCommand()); //…… } } public class AddCommand implements Command { public void process(int[] target){ int sum = 0; for(int tmp:target){ sum+= tmp; } System.out.println("数组元素和是:"+sum); } } public class PrintCommand implements Command { public void process(int[] target){ for(int tmp: target){ System.out.println("打印"+tmp); } } }
6.7 内部类
有时候会把一个类放在另一个类内定义,这种定义在其他类内部的类就叫内部类。
内部类有如下作用:
1、提供更好封装,将内部类隐藏在外部类之内,不允许同一个包的其他类访问。
2、内部类成员可以直接访问外部类的私有数据,因为内部类被当做外部类成员。
3、匿名内部类适合创建只使用一次的类。
从语法上来看,内部类除了定义在外部类里面,还有2点区别:
1、内部类比外部类可以多使用3个修饰符:private, protected, static
2、非静态内部类不能拥有静态成员。
6.7.1 非静态内部类
定义:在一个类内定义一个类。
@如果外部类成员变量,内部类成员变量和 内部类方法局部变量重名,可以使用this, 外部类名.this. 限定区分。
@非静态类可以直接访问外部类的private成员,但反过来不成立。
6.7.2 静态内部类
static修饰一个内部类,则这个内部类属于外部类本身,而不属于外部类的某个对象。
静态内部类只能访问外部的静态成员。
6.7.3 使用内部类
3中内部类的用法
1、在外部类中使用内部类,和平常使用类没有太多区别。
唯一区别是。不要在外部类静态成员中使用非静态内部类。@静态成员不能访问非静态成员
2、在外部类以外使用非静态内部类
如果要在外部类以外使用内部类,则内部类不能是private的。
在外部类以外的地方定义内部类语法:
OutterClass.InnerClass varName
由于非静态内部类的对象必须寄生在外部类的对象里,因此创建非静态内部类对象前,要先创建外部类对象。
在外部类以外的地方创建非静态内部类实例语法:
OuterInstance.new InnerConstructor()
创建非静态内部类的子类时,必须保证子类构造器可以调用非静态内部类的构造器,
调用非静态内部类的构造器时,必须存在一个外部类对象。
3、在外部类之外使用静态内部类
创建:
new OuterClass.InneerConstructor()
创建静态内部类的子类无需用到外部类,因此比较简单
public class StaticSubClass extends StaticOut.StaticIn
{…}
6.7.4 局部内部类
在方法里定义的一个类。仅在方法中有效,不能用访问控制符和static修饰。
6.7.5 Java8 改进的匿名内部类
适合创建只需要使用一次的类。
常见匿名内部类的语法:
new 实现接口()| 父类构造器(实参表)
{匿名类的类体}
创建匿名类或立即创建一个实例。
最常用的用法是创建某个接口类型的对象
6.8 Java 8 新增的lambda表达式
Lambda表达式支持将代码块作为方法参数,允许使用简洁的代码来创建一个只有抽象方法的接口实例。
//使用内部匿名类 public class CommandTest { public static void main(String[] args){ ProcessArray pa = new ProcessArray(); int [] target = {1,3,5,5}; 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 CommandTest { public static void main(String[] args){ ProcessArray pa = new ProcessArray(); int [] array = {1,3,5,5}; pa.process(array, (int[] target)->{ int sum = 0; for(int tmp:target){ sum+=tmp; } System.out.println("数组和="+sum); }); } }
Lambda表达式主要作用就是代替匿名内部类的繁琐语法。它由三部分组成:
形参列表。 允许省略形参类型,如果只有一个参数,圆括号也可以省略
箭头 ->
代码块 如果只有一条语句,允许省略花括号。(如果)Lambda代码块只有一条return语句,可以省略return 关键字。
6.8.2 Lambda表达式与函数式接口
Lambda表达式的类型,也被称为目标类型,Lambda表达式的目标类型必须是 函数式接口。
函数式接口代表只包含一个抽象方法的接口。
如果采用匿名内部类实现,则只需要实现一个抽象方法,这种情况就可以使用Lambda表达式来创建对象。
Lambda表达式的结果被当成对象,因此程序中可以使用Lambda表达式进行赋值。
为了保证Lambda表达式的目标类型是一个明确的函数式接口,可采用如下三种形式
1、将Lambda表达式赋值给函数式接口类型的变量
2、将Lambda表达式作为函数式接口类型的参数传递给某个方法
3、使用函数式接口对Lambda表达式进行强制类型转换
6.8.3 方法引用与构造器引用
如果Lambda表达式的代码块只有一条代码,还可以在代码块中使用方法引用和构造器引用。
6.8.4 Lambda表达式和匿名内部类的联系和区别
Lambda表达式是匿名内部类的一种简化,可以部分取代匿名内部类。
共同点:
1、都可以直接访问“effective final“ 局部变量,以及外部类的成员变量。
2、Lambda表达式创建的对象和匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法。
区别:
1、匿名内部类可以为任意接口创建实例,不管接口有多少抽象方法,但Lambda只能为函数式接口创建实例
2、匿名内部类可以为抽象类甚至普通类创建实例,但Lambda只能为函数式接口创建实例
3、匿名内部类可以实现的抽象方法的方法体允许调用接口中定义的默认方法,但Lambda表达式的代码块不允许。
6.8.5 使用Lambda表达式调用Arrays的类方法
Arrays类的某些方法需要Comparator XxxOperator XxxFunction等接口的实例。
import java.util.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}; Arrays.parallelPrefix(arr2, (left,right)->left*right); System.out.println(Arrays.toString(arr2)); } }
6.9 枚举类
实例有限且固定。
6.9.2 枚举类入门
enum关键字,用于定义枚举类。
枚举类可以实现接口,使用enum定义的枚举类默认继承了java.lang.Enum类,而不是继承Object类。
使用enum定义、非抽象的枚举类默认使用final修饰,不能派生子类
枚举类的构造器只能用private修饰,如果省略,默认private
枚举类的实例必须在第一行显式列出,否则永远无法产生实例。这写实例系统会自动添加public static final。
枚举类默认提供values()方法,可以遍历所有枚举值。
如果需要使用枚举类的某个实例,可以使用EnumClass.variable 形式,如SeasonEnum.SPRING
定义SeasonEnum类
public enum SeasonEnum { SPRING,SUMMER,FALL,WINTER; } public class EnumTest { public void judge(SeasonEnum s){ 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; default: break; } } public static void main(String[] args){ for(SeasonEnum s: SeasonEnum.values()){ System.out.println(s); } new EnumTest().judge(SeasonEnum.SPRING); } }
6.9.3 枚举类的成员变量、方法和构造器
建议枚举类的成员变量为private final。
一旦为枚举类显示的定义了带参数的构造器,列出枚举值时就必须传入对应参数。
6.9.4 实现接口的枚举类
和普通类实现接口类似,
如果需要每个枚举类实现方法呈现不同的行为,可以让每个枚举值分别实现该方法。
例:
//接口 public interface GenderDesc { void info(); } //实现接口 public enum Gender implements GenderDesc { MALE(“男”) { public void info(){ System.out.println(“男性”); } }, FEMALE(“女”) { public void info(){ System.out.println(“女性”); } }; private final String name; //…一些方法 public void setName(String name) //构造器 private Gender(String name){this.name = name;}
6.9.5 包含抽象方法的枚举类
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(3, 4)); System.out.println(Operation.TIMES.eval(3, 4)); System.out.println(Operation.DIVIDE.eval(3, 4)); } }
6.10 对象与垃圾回收
当程序创建对象、数组等引用类型实体时,系统都会在堆内存为之分配一块内存,当这块内存不被任何引用变量引用时,这块内存就变成垃圾,
等待垃圾回收机制回收。
6.10.1 对象在内存中的状态
可达状态:对象创建后,有一个及以上的引用变量引用。
可恢复状态:不再有变量引用它,进入可恢复状态。垃圾回收机制准备回收。在回收之前,系统会调用可恢复对象的finalize()方法进行资源清理。
如果调用finalize()方法时重新让变量引用该对象,则这个对象会变成可达状态,否则进入不可达状态。
不可达状态:对象于引用变量之间的关系被切断,对象将永久失去引用。系统回收对象占有的资源。
6.10.2 强制垃圾回收
这种强制只是通知系统进行垃圾回收,但是系统是否回收仍然不确定。
强制系统垃圾回收有两种方法:
System.gc()
Runtime.getRuntime().gc()
6.10.3 finalize方法
该方法是定义在Object类里的实例方法。原型为
protected void finalize() throws Throwable
永远不要主动调用finalize方法,该方法由垃圾回收机制调用。
finalize()方法何时调用不确定,甚至程序结束都未必执行。
6.10.4 对象的软、弱和虚引用
1、强引用
最常见的引用方式,创建一个对象,把对象给一个引用变量,通过变量操作对象。
2、软引用(SoftReference)
通过SoftReference 类实现,一个对象只有软引用时,可能被垃圾回收机制回收。
对于只有软引用的对象而言,当系统内存充足时,不会被回收;内存不足时,可能会被回收。
3、弱引用(WeakReference)
通过WeakReference类实现,与软引用类似,但级别更低。
系统运行垃圾回收机制时,总会回收该对象的内存。
4、虚引用(PhantomReference)
通过PhantomReference类实现,类似于没有引用。
对对象本身没有太大影响,主要用于跟踪对象被垃圾回收的状态。
虚引用不能单独使用,必须和引用队列(ReferenceQueue)联合使用。
引用队列用于保存被回收后对象的引用。
联合使用软引用、弱引用和引用队列时,系统在回收被引用对象之后,将把被回收对象的引用添加到关联的引用队列中,
虚引用和引用队列关联时,会在回收之前,将把被回收对象的引用添加到关联的引用队列中。
6.11 修饰符的适用范围
6.12使用JAR文件
JAR全程java Archive File,意思是Java档案文件。
通常JAR是一种压缩文件,和ZIP文件兼容,也称JAR包。
JAR文件中默认包含一个META-INF/MANIFEST.MF的清单文件。
6.12.1 jar命令
1、创建JAR文件 jar cf test.jar test
2、创建JAR文件,显示压缩过程 jar cvf test.jar test
3、不使用清单文件 jar cvfM test.jar test
4、自定义清单文件内容 jar cvfm test.jar manifest.mf test
清单文件是一个文本文件,内容由多个key-value对组成
格式
key:<空格>value
文件一行一对,开头不空行,结尾空一行
假设保存为a.txt, 使用如下命令提取键值对到META-INF/MANIFEST.MF文件中:
jar cvfm test.jar a.txt test
5、查看JAR包内容: jar tf test.jar
当JAR内容非常多时,输出到文档 jar tf test.jar >a.txt
6、查看JAR包详细内容 jar tvf test.jar
7、解压缩 jar xf test.jar
8、 带提示解压缩 jar xvf test.jar
9、更新JAR文件 jar uf test.jar Hello.class
10、更新时显示详细信息 jar uvf test.java Hello.class
6.12.2 创建可执行JAR包
应用程序开发完成后,有如下发布方式:
1、为应用编辑一个批处理文件,如
java package.MainClass
如果不想保留命令行,可以使用
start javaw package.MainClass
2、将应用程序打包成可执行JAR包,//运行时应该在test目录的上一层进入命令行
jar cvfe test.jar test.Test test
//指定主类是test.Test类,为程序入口
运行jar包:
java -jar test.jar创建JAR包时,类必须放在和包结构对应的目录结构中。
6.12.3 关于JAR包的技巧
可以使用压缩软件直接解压