Java高级特性

一、基础概念

1. 抽象

抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。就如在设计时的一个父类,主要用在了继承和接口这两个应用广泛的案列。

2.继承

其实质就是用到了桥梁模式(@http://www.cnblogs.com/java-my-life/archive/2012/05/07/2480938.html)抽象出共同的属性,减少冗余重复的内容。使设计更清新明了。

继承中的注意事项
1.子类重写父类中的方法,可以扩大修饰范围,但不能缩小修饰范围
如:

class Base {
public Base (){ //... 
	}

public Base ( int m ){ //...
	}

protected void fun( int n ){
	//... 
}

public class Child extends Base{
// member methods
编译通过
//	public void fun ( int n ) {
//		//... }
//	}
编译不通过
	private void fun( int n ){ 
		//...}
	}
}

1.直接先看代码案列

/父类
package com.imooc;
public class Cleanser {
	private String s = new String("Cleanser");
	public void append(String a) { s += a; }
	public void dilute() { append(" dilute()"); }
	public void apply() { append(" apply()"); }
	public void scrub() { append(" scrub()"); }
	public void print() { System.out.println(s); }	
//子类

package com.imooc;

public class Detergent extends Cleanser {
	@Override
	public void scrub() {
		append(" Detergent.scrub()");
	//	super.scrub(); // Call base-class version
		}
	
	public void foam() { append(" foam()"); }
}

//子类1

public class Detergent2 extends Cleanser {
	@Override
	public void scrub() {
		append(" Detergent2.scrub()");
	//	super.scrub(); // Call base-class version
		}
	
	public void foam() { append(" foam2()"); }
}	
public static void main(String[] args) {
	Cleanser x = new Cleanser();
	x.dilute(); x.apply(); x.scrub();
	x.print();
	}
}

测试类:创建一个带有继承关系的对象

//测试类1

public class testCleanser {

	@Test
	public void test() {
		Cleanser x = new Detergent();   //x为父类对象并派生出特有子类,不能直接调用子类的方法,x为向上转型
		
//		x.dilute();
//		x.apply();
		x.scrub();  //默认调用的是子类重写后的方法
		((Detergent) x).foam();//运行不报错
		x.print();
		System.out.println("Testing base class:");
	//	 Cleanser.main(args);
	}
//测试类1-1
public class testCleanser {

	@Test
	public void test() {
	
		//x为父类对象并派生出特有子类,不能直接调用子类的方法
		Cleanser x=new Detergent2();
		//x1为父类对象并派生出特有子类,不能直接调用子类的方法
		Cleanser x1=new Detergent();
		x.dilute();
//		x.apply();
		x.scrub();
		x1.scrub();
		((Detergent2) x).foam();
		((Detergent) x1).foam();
		x.print();
		System.out.println("-----------");
		x1.print();
		System.out.println("Testing base class:");
	//	 Cleanser.main(args);
	}

输出为:

Cleanser dilute() Detergent2.scrub() foam2()
-----------
Cleanser Detergent.scrub() foam()
Testing base class:

是不是感觉就如接口中的一个接口对应着不同的实现类,分别调用重写后的方法,只不过接口中的方法都是抽象方法罢了。
在来看一下:
直接new一个子类对象

/测试类2

public class testCleanser {

	@Test
	public void test() {
		Detergent x = new Detergent();//x为子类对象,可以直接调用父类的(非私有)方法
		
		x.dilute();
//		x.apply();
		x.scrub();
//		((Detergent) x).foam();
		x.print();
		System.out.println("Testing base class:");
	//	 Cleanser.main(args);
	}

}

直接new一个父类对象

//测试类3

public class testCleanser {

	@Test
	public void test() {
	
		Cleanser x=new Cleanser();//x为父类对象,不能调用子类的方法
		x.dilute();
//		x.apply();
		x.scrub();
		((Detergent) x).foam();//运行报错,java.lang.ClassCastException: com.imooc.Cleanser cannot be cast to com.imooc.Detergent

		x.print();
		System.out.println("Testing base class:");
	//	 Cleanser.main(args);
	}

}

3.封装

即对外只提供接口(告诉消费者这个接口是干什么的),而实现细节隐藏起来由提供方实现。

这样有利于代码的阅读和简洁,以及消费者更简单的去操作。

4.多态

什么是多态?


多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。

要使用多态必须满足三个必要条件
一、要有继承;
二、要有重写;
三、父类引用指向子类对象。

即多态的使用场景主要就是重载和重写
可以参考如下代码来做一些进一步的理解

/**
 * A中的方法为重载
 * Created by daiwei on 2017/10/21.
 */
public class A {
    public String show(D obj){
        return ("A and D");
    }
    public String show(A obj){
        return ("A and A");
    }
    public static void main(String[] args) {
        A a1 = new A();
        //new 的是b对象,但a2已声明是A类型
        A a2 = new B();
        B b = new B();
        C c = new C();
        D d = new D();
        /**
         * 因为new 的是A对象只会调用父类的方法,无论是b还是c都可以上溯到A类型。
         * 因为d对象已经能绑定到A中的方法了,顾无需再上溯造型
         */
        System.out.println(a1.show(b));
        System.out.println(a1.show(c));
        System.out.println(a1.show(d));
        /**
         * 因为new 的是b对象,故方法会执行b中重写的方法,
         * 只有在子类没有此方法而父类有时才会调用父类的方法
         * a2.show(b)和a2.show(c)因为a2已声明是A类型,即只会执行A类型的相关方法,
         * 而不会去执行A中没有的方法。
         */
        System.out.println(a2.show(b));
        System.out.println(a2.show(c));
        System.out.println(a2.show(d));
        /**
         * 因为b中没有引用父类对象故b上溯到b,c上溯到b
         */
        System.out.println(b.show(b));
        System.out.println(b.show(c));
        System.out.println(b.show(d));
    }
}
class C extends B{}
class D extends B{}
class B extends A {
    /**
     * 此方法并没有重写父类方法
     * @param obj
     * @return
     */
    public String show(B obj) {
        return ("B and B");
    }

    @Override
    public String show(A obj) {
        return ("B and A");
    }
}


执行结果为:

A and A
A and A
A and D
B and A
B and A
A and D
B and B
B and B
Disconnected from the target VM, address: '127.0.0.1:50477', transport: 'socket'
A and D

多态是java最最核心的特性,封装和继承几乎都是为多态而准备的,在说多态之前先说说静态绑定和动态绑定

绑定: 把一个方法与其所在的类/对象 关联起来叫做方法的绑定。

绑定分为静态绑定(前期绑定)和动态绑定(后期绑定)。

1.静态绑定

静态绑定(前期绑定)是指:在程序运行前就已经知道方法是属于那个类的,在编译的时候就可以连接到类的中,定位到这个方法。 在Java中,final、private、static修饰的方法以及构造函数都是静态绑定的,不需程序运行,不需具体的实例对象就可以知道这个方法的具体内容。

2.动态绑定

动态绑定(后期绑定)是指:在程序运行过程中,根据具体的实例对象才能具体确定是哪个方法。

《corejava》是这样描述静态绑定和动态绑定的:

这里写图片描述

静态方法重载是静态绑定,方法调用是通过:类名.方法。普通方法重载是动态绑定。而重写都是动态绑定。
动态绑定的优点:主要是增加了代码的可扩展性,以下引用于corejava

这里写图片描述

3.方法重载和方法重写

方法重载:

编译时的多态性(也称为前绑定)-决定于方法的参数,方法名相同而参数的类型,顺序或者个数不同。
调用一个方法由参数的类型和参数的个数来决定到底如何去实现这个方法。

方法返回值和访问修饰符可以不同,发生在编译时。

public int add(int a,String b)
public String add(int a,String b) //编译报错

方法重写:

发生在父子类中,方法名、参数列表必须相同。

运行时的多态性(也称为后绑定)-决定于new的对象,使用@override注解进行标记。
方法的执行由new 的对象来决定,new 的对象不同在调用此方法时执行的结果也会有不同。即在运行期间来绑定具体使用哪一个方法。

5.对象的声明和对象的创建

对象声明只是告诉大家他是属于那种类型,就如指针一样没有具体的值。
而对象创建就是为这个已声明的类型赋予实际的空间就是实际值。

故对象的声明和对象的创建是不同的,是有先后顺序的,先声明后创建。

举个例子
Person p1=new Person();语句实际上同时完成了对象的声明与创建,可以分开这样写:
Person p1;
p1=new Person();
声明对象只在栈中声明指针类型的变量,不在内存中存储具体的数值,而只存放另一块堆中内存的地址。创建对象在java中一般用new关键字, p1=new Person();这行代码一共做了两件事情:第一件是在堆中分配一块存放学生具体数值的内存,第二件是把这个内存的首地址赋给上面声明的指针变量。下面我们就可以通过对象名.属性名访问具体属性了。对象必须创建后才能使用,如果只声明不创建,那么调用对象属性和方法时将会报空指针异常(NullPointerException)。

6.什么是面向对象编程?

对比面向过程,是两种不同的处理问题的角度。

面向过程更注重事情的每一个步骤及顺序,面向对象更注重事情有哪些参与者模型(对象)、及各自需要做什么(方法),及各自含有的特征(属性)。

面向对象编程我个人理解就是:把一些特有的特征(属性)和行为(方法)封装在一个类里面,我们要去使用某个属性或者方法,就通过这个类来进行使用。

比如:洗衣机洗衣服
面向过程会将任务拆解成一系列的步骤(函数),1、打开洗衣机----->2、放衣服----->3、放洗衣粉>4、清洗----->5、烘干
面向对象会拆出人和洗衣机两个对象:
人:打开洗衣机 放衣服 放洗衣粉
洗衣机:清洗 烘干
从以上例子能看出,面向过程比较直接高效,而面向对象更易于复用、扩展和维护

7.类的初始化过程:

1. 父类静态成员和静态初始化块 ,按在代码中出现的顺序依次执行
2. 子类静态成员和静态初始化块 ,按在代码中出现的顺序依次执行
3. 父类实例成员和实例初始化块 ,按在代码中出现的顺序依次执行
4. 父类构造方法
5. 子类实例成员和实例初始化块 ,按在代码中出现的顺序依次执行
6. 子类构造方法

结论:对象初始化的顺序,先静态方法,再构造方法,每个又是先基类后子类。


8.继承和聚合

继承关系即is a 关系,子类继承父类的属性 方法;比如:我 is a 人;再比如菱形、圆形和方形都是形状的一种,那么他们都应该从形状类继承而不是聚合/组合关系。

聚合/组合关系即has a关系,两个对象之间是整体和部分的关系;比如:我 has a 头;再比如电脑是由显示器、CPU、硬盘这些类聚合成电脑类,而不是从电脑类继承。

9.Java判断值相等

string类型用equals,数字类型用compareTo

使用equals

import java.math.BigDecimal;
/*
*02、如果用equals比较的话,chatGPT举一个错误的例子并告诉你1.0和1.00用equals比较是正确的(如下图),不过不要盲目相信AI,要自己动手试一下,结果是否定的。
*/
public class CompareBigDecimal {
    public static void main(String[] args) {
        BigDecimal a = new BigDecimal("1.0");
        BigDecimal b = new BigDecimal("1.00");
        boolean result = a.equals(b);
        if (result) {
            System.out.println("a和b相等");
        } else {
            System.out.println("a不等b");
        }
    }
}

运行结果:a不等b

使用compareTo

import java.math.BigDecimal;
/*
*03、如果用compareTo比较的话,
*/
public class CompareBigDecimal {
    public static void main(String[] args) {
        BigDecimal a = new BigDecimal("1.0");
        BigDecimal b = new BigDecimal("1.00");
        int result = a.compareTo(b);
        if (result == 0) {
            System.out.println("a和b相等");
        } else if (result > 0) {
            System.out.println("a大于b");
        } else {
            System.out.println("a小于b");
        }
}       

运行结果;a和b相等

问:为什么要用呢compareTo而不是equals呢?

在Java中,使用equals()方法进行比较时,比较的是对象的内容是否相等。
对于BigDecimal类型的对象,虽然它们的值相等,但是它们的精度可能不同,这个时候equals()方法会返回false。
例如,new BigDecimal(“1.0”)和new BigDecimal(“1.00”)虽然值相等,但是精度不同,equals()方法会返回false,而使用compareTo则会返回true。

二、泛型

1.背景

我们都知道,继承是面向对象的三大特性之一,比如在我们向集合中添加元素的过程中add()方法里填入的是Object类,而Object又是所有类的父类,这就产生了一个问题——添加的类型无法做到统一 由此就可能产生在遍历集合取出元素时类型不统一而报错问题。

例如:我向一个ArrayList集合中添加Person类的对象,但是不小心手贱添加了一个Boy类的对象,这就会导致如下结果

传统的方式不能对加入到集合ArrayList中的数据类型进行约束(不安全)遍历的时候,需要进行类型转换,如果集合中的数据量较大,对效率有影响 这就极大地降低了程序的健壮性,因此设计者针对此问题引入了泛型!

所以使用泛型有以下好处

1.编译检测

针对上述问题,当我们采用泛型就会显得非常简单,只需要在编译类型后利用泛型指定一个特定类型,编译器就会自动检测出不符合规范的类并抛出错误提示

2.减少了类型转换的次数,提高效率

  • 当不使用泛型时:

  • 当使用泛型时:

2.原理

以下来自《码农翻身》

IO大臣说:“陛下圣明,臣愚钝,还有一事不明,这个所谓的泛型,怎么实现呢?”

C++泛型使者说: “在我们C++帝国,每次你去实例化一个泛型/模板类都会生成一个新的类,例如模板类是List ,然后你用int ,double,string, Employee 分别去实例化, 那编译的时候,我们就会生成四个新类出来,例如List_int和List_double,List_string, List_Employee。” 

集合框架大臣说:“啊?! 这样一来得生成很多新的类出来啊,系统会不会膨胀得要爆炸了。”

国王说:“不用担心,我已经给C++的泛型使者深谈过,我们不用膨胀法, 相反,我们用擦除法。”

“擦除法? ” 众大臣面面相觑。 

“简单来说就是一个参数化的类型经过擦除后会去除参数, 例如ArrayList<T> 会被擦除为ArrayList”

“那我传入的String,Integer等都消失了?”  集合框架大臣大惊失色。

“不会的,我会把他们变成Object ,  例如ArrayList<Integer>其实被擦除成了原始的ArrayList :

图片

线程大臣问道: “陛下, 我们通过泛型, 本来是不想让臣民们写那个强制转型的,臣民们可以写成这样 Integer i = list1.get(0);   现在类型被擦除,都变成Object了, 怎么处理啊? ”

Java国王说: “ 很简单啊, 在编译的时候做点手脚,加个自动的转型嘛: Integer i = (Integer)list1.get(0);”

“陛下真是高瞻远瞩, 臣等拜服”  IO大臣马上拍马屁。

3.泛型接口

曾经写接口的时候都没有定义泛型,它默认的就是Object类,其实这样写是不规范的!

如果说接口的存在是一种规范,那泛型接口就是规范中的规范

interface Im<U,R>{    void hi(R r);    void hello(R r1,R r2,U u1,U u2);    default R method(U u){        return null;    }}

在上述的泛型接口中已经规定传入其中的必须是U,R类的对象,那么当我们传入其他类的对象时就会报错,如图:

 根据规则,当我们实现接口时,就必须实现他的所有方法,而在这时我们就可以向<U,R>中传入我们自己规定的类。在IDEA中重写接口中的方法时,编译器会自动将<U,R>替换成我们事先规定的类。

4.泛型继承

经过了几个月的准备, Java泛型正式推出,开始让臣民们使用了。 

不出国王和大臣所料, 泛型极大程度地减少了运行期那些转型导致的异常,简化了代码,受到了大家的一致欢迎。 

国王特地设置了一个泛型大臣的职务, 暂时让集合框架大臣兼任, 没办法,集合框架的改动是泛型的一个重头戏。

过了几天, 泛型大臣兼集合框架大臣上了一个奏章,上面有一张图和若干代码:

图片

图片

国王觉得很诧异,这是怎么回事,print函数能接受的参数不是ArrayList<Fruit>吗? 当传递一个ArrayList<Apple>为什么出错呢, 难道我们Java帝国的多态不管用了吗?

他召来泛型大臣问个明白。 

泛型大臣说:“陛下明鉴,这个Apple 虽然是Fruit的子类, 但是 ArrayList<Apple>却不是 ArrayList<Fruit>的子类,实际上他们俩之间是没有关系的,不能做转型操作,所以调用print的时候就报错了。”

图片

“为什么不能让ArrayList<Apple>转成ArrayList<Fruit>呢?  ”

“如果可以这么做的话, 那么不但可以向这个list中加入Apple, 还可以加入Orange, 泛型就被破坏了”

图片

“奥,原来如此”  国王心想泛型大臣还是不错滴。 “那针对刚才的问题怎么办呢?”

“我和各位大臣商量了,我们打算引入一个通配符的方式来解决, 把函数的输入参数改为改成下面这样:”

图片

“也就是说,传进来的参数,只要是Fruit或者Fruit的子类都可以,对吧”  国王看出了关键。

“是的,陛下,这样以来就可以接收ArrayList<Fruit> 和 ArrayList<Apple> ,ArrayList<Orange> 这样的参数了!”

“好吧,虽然看起来有点不爽, 就这么实施吧!”

5.泛型方法

集合框架大臣说: “陛下,刚才您说的都是泛型类, 对于一些静态方法该怎么办?”

图片

图片

“简单啊,把那个<T>移到方法上去!” 国王的命令不容置疑

图片

图片

集合框架大臣看了一会,自言自语到: “这个静态的函数是求最大值的,就是说需要对List中的元素比较大小,如果臣民们传入的T没有实现Comparable接口,就没法比较大小了!”

线程大臣,IO大臣纷纷点头称是。

王国心想这些大臣也不是一无是处,还是有点想法的嘛, 他转向C++的使者:  “这倒是个难题, 泛型使者, 你怎么看?”

“这个容易,可以做一个类型的限制, 让臣民们传入类型T必须是Comparable的子类才行, 要不然编译器就报错, 我建议使用extends关键字。” C++的泛型使者看起来很有经验。

图片

图片

“妙啊” 国王大为赞赏  “来人, 赏金500两! ”

IO大臣提议: “陛下,臣提议让泛型使者在京城多呆几天,协助我们把Java泛型给实现了。”

国王说:“准奏,这是一件大事情, 希望各位爱卿同心协力, 办好后朕还有重赏。”

除了extends之外, Java泛型还支持super,   实际上为了更加灵活,上面的Comparable<T> 应该写成Comparable <? super T> , 这里不再展开描述。 

参考资料

1.《码农翻身》
3.《corejava》
4.java提高篇(四)-----理解java的三大特性之多态http://www.cnblogs.com/chenssy/p/3372798.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值