2、面向对象

面向对象

面向过程和面向对象

面向过程(Procedure Oriented)和面向对象(Object Oriented,OO)都是对软件分析、设计和开发的一种思想,它指导着人们以不同的方式去分析、设计和开发软件。

早期先有面向过程思想,随着软件规模的扩大,问题复杂性的提高,面向过程的弊端越来越明显的显示出来,出现了面向对象思想并成为目前主流的方式

两者都贯穿于软件分析、设计和开发各个阶段,对应面向对象就分别称为面向对象分析(OOA)、面向对象设计(OOD)和面向对象编程(OOP)。C语言是一种典型的面向过程语言,Java是一种典型的面向对象语言。
在这里插入图片描述  面向过程思想思考问题时,我们首先思考“怎么按步骤实现?”并将步骤对应成方法,一步一步,最终完成。 这个适合简单任务,不需要过多协作的情况下。比如,如何开车?我们很容易就列出实现步骤:

  1. 发动车 2. 挂挡 3.踩油门 4. 走你

面向过程适合简单、不需要协作的事务。 但是当我们思考比较复杂的问题,比如“如何造车?”,就会发现列出1234这样的步骤,是不可能的。那是因为,造车太复杂,需要很多协作才能完成。此时面向对象思想就应运而生了。

面向对象(Object)思想更契合人的思维模式。我们首先思考的是“怎么设计这个事物?” 比如思考造车,我们就会先思考“车怎么设计?”,而不是“怎么按步骤造车的问题”。这就是思维方式的转变。

一、面向对象思想思考造车,发现车由如下对象组成:

  1. 轮胎
  2. 发动机
  3. 车壳
  4. 座椅
  5. 挡风玻璃

为了便于协作,我们找轮胎厂完成制造轮胎的步骤,发动机厂完成制造发动机的步骤;这样,发现大家可以同时进行车的制造,最终进行组装,大大提高了效率。但是,具体到轮胎厂的一个流水线操作,仍然是有步骤的,还是离不开面向过程思想!

因此,面向对象可以帮助我们从宏观上把握、从整体上分析整个系统。 但是,具体到实现部分的微观操作(就是一个个方法),仍然需要面向过程的思路去处理。

我们千万不要把面向过程和面向对象对立起来。他们是相辅相成的。面向对象离不开面向过程!

面向对象和面向过程的总结:

1、都是解决问题的思维方式,都是代码组织的方式。

2、解决简单问题可以使用面向过程

3、解决复杂问题:宏观上使用面向对象把握,微观处理上仍然是面向过程

面向对象思考方式:

遇到复杂问题,先从问题中找名词,然后确立这些名词哪些可以作为类,再根据问题需求确定的类的属性和方法,确定类之间的关系。

建议:

1.面向对象具有三大特征:封装性、继承性和多态性,而面向过程没有继承性和多态性,并且面向过程的封装只是封装功能,而面向对象可以封装数据和功能。所以面向对象优势更明显。

2.一个经典的比喻:面向对象是盖浇饭、面向过程是蛋炒饭。盖浇饭的好处就是“菜”“饭”分离,从而提高了制作盖浇饭的灵活性。饭不满意就换饭,菜不满意换菜。用软件工程的专业术语就是“可维护性”比较好,“饭” 和“菜”的耦合度比较低。

 1.对象说白了也是一种数据结构(对数据的管理模式),将数据和数据的行为放到了一起。

 2.在内存上,对象就是一个内存块,存放了相关的数据集合!

 3.对象的本质就一种数据的组织方式!

对象和类的概念

类可以看做是一个模版,或者图纸,系统根据类的定义来造出对象。我们要造一个汽车,怎么样造?类就是这个图纸,规定了汽车的详细信息,然后根据图纸将汽车造出来。

我们叫做class。

对象我们叫做Object,instance(实例)。以后我们说某个类的对象,某个类的实例。是一样的意思。

我们平时用new创建一个对象是调用了类的构造方法,如果类没有构造方法,调用的就是类本身默认的无参构造方法

总结:

1.对象是具体的事物;类是对对象的抽象;

2.类可以看成一类对象的模板,对象可以看成该类的一个具体实例。

3.类是用于描述同一类型的对象的一个抽象概念,类中定义了这一类对象所应具有的共同的属性、方法

第一个类的定义

类的定义方式

// 每一个源文件必须有且只有一个public class,并且类名和文件名保持一致!
//类与类之间可以互相引用、嵌套。
public class Car { 
}
class Tyre { // 一个Java文件可以同时定义多个class
}
class Engine {
}
class Seat {
}

上面的类定义好后,没有任何的其他信息,就跟我们拿到一张张图纸,但是纸上没有任何信息,这是一个空类,没有任何实际意义。所以,我们需要定义类的具体信息。

对于一个类来说,一般有三种常见的成员:属性field、方法method、构造器constructor。这三种成员都可以定义零个或多个

class SxtStu {
    //属性(成员变量)
    int id;
    String sname;
    int age;  
    //方法
    void study(){
        System.out.println("我正在学习!");
    }  
    //构造方法,不写的话默认也是这个无参的构造方法
   SxtStu(){
   }
}

属性(field,或者叫成员变量)

属性用于定义该类或该类对象包含的数据或者说静态特征。属性作用范围是整个类体

在定义成员变量时可以对其初始化,如果不对其初始化,Java使用默认的值对其初始化

数据类型默认值
整型0
浮点型0.0
字符型‘\u0000’
布尔型false
所有引用类型null

属性定义格式:

[修饰符]  属性类型  属性名 = [默认值] ;

方法

方法用于定义该类或该类实例的行为特征和功能实现

方法是类和对象行为特征的抽象。方法很类似于面向过程中的函数。

面向过程中,函数是最基本单位,整个程序由一个个函数调用组成。

面向对象中,整个程序的基本单位是类,方法是从属于类和对象的。

方法定义格式:

[修饰符]  方法返回值类型  方法名(形参列表) {
    // n条语句
}

一个典型类的定义和UML图

class  Computer {
	String  brand;
	void start() {
		System.out.println(brand+"电脑开启!");
	}
}
class Stu {
	
	//属性fields
	int  id;
	String  sname;
	int  age;
	
	//引用其他类:Computer
	//这里有引用类,下面的study方法才可以使用引用类的方法和属性
    //这里的这个类看做是一个属性,一个成员变量
	Computer  comp;  
	
	//方法
	void  study(){
		//引用了其他类Computer的方法和属性
		comp.start();
		System.out.println("我在认真学习!!使用电脑:"+comp.brand);
	}
	
	void play() {
		System.out.println("我在玩游戏");
	   }
}
public class Test {
	public static void main(String[] args) {
		//创建学生对象
		//如果不为属性赋值,那么引用类型默认为null,数字默认为0
		Stu  stu = new Stu();   
		stu.id=1001;
		stu.sname= "靓仔";
		stu.age = 21;
		
		//创建电脑对象
		Computer  c1 = new Computer();
		c1.brand = "联想";
		
		//将电脑对象与学生对象连接
        //实际就是给对象的这个成员变量赋值
		stu.comp = c1;
		
		//如果上面没有创建电脑对象,没有将电脑对象和学生对象连接
		//那么就不可以将使用study()方法,但是play()方法可以使用
		stu.study();
		stu.play();
	}
}
结果:
联想电脑开启!
我在认真学习!!使用电脑:联想
我在玩游戏

在这里插入图片描述

面向对象的内存分析

可以参考的网页:

java实例化一个类时在堆和栈中保存了那些数据

Java虚拟机的内存可以分为三个区域:stack、heap、方法区(属于堆)method area。

栈的特点如下:

  1. 栈描述的是方法执行的内存模型。每个方法被调用都会创建一个栈帧(存储局部变量、操作数、方法出口等)

  2. JVM为每个线程创建一个栈,用于存放该线程执行方法的信息(实际参数、局部变量等)

    (个人想法:每个为每个线程创建一个栈,然后为线程的每个方法创建一个栈帧,意思就是一个栈里面有多个栈帧)

  3. 栈属于线程私有,不能实现线程间的共享!

  4. 栈的存储特性是“先进后出,后进先出”

  5. 栈是由系统自动分配,速度快!栈是一个连续的内存空间!

堆的特点如下:(堆其实就是一个完全二叉树:大根堆,小根堆)

当我们每次都要取某一些元素的最小值,而取出来操作后要再放回去,重复做这样的事情。
我们若是用快排的话,最坏的情况需要O(q*n^2),而若是堆,仅需要O(q*logn)

  1. 堆用于存储创建好的对象和数组(数组也是对象)
  2. JVM只有一个堆,被所有线程共享
  3. 堆是一个不连续的内存空间,分配灵活,速度慢!

方法区(又叫静态区)特点如下:

  1. JVM只有一个方法区,被所有线程共享!

方法区实际也是堆,只是用于存储常量相关的信息!

  1. 用来存放程序中永远是不变或唯一的内容。(类信息【Class对象】、静态变量字符串常量等)
class Computer {
    //属性
    String brand;  
}
public class SxtStu {
    // 属性
    int id;
    String sname;
    int age;
    Computer comp;
    //方法
    void study() {
        System.out.println("我正在学习!使用我们的电脑,"+comp.brand);
    }
    SxtStu() {
    }
    //程序执行的入口,必须要有
    //先用 javac Sxtstu.java 将源文件编译成字节码文件 Sxtstu.class,
    //之后用java Sxtstu.class执行这个类,此时这整个Sxtstu类的相关信息:代码、静态变量、静态方法、字符串常量会加载到方法区中
    public static void main(String[] args) {
        SxtStu stu1 = new SxtStu();
        stu1.sname = "张三";
        Computer comp1 = new Computer();
         comp1.brand = "联想";
        stu1.comp = comp1;
        stu1.study();
    }
}

在这里插入图片描述
在这里插入图片描述

执行过程:

先通过javac 将原来的xxx.java 程序编译成字节码文件:xxx.class

当我们执行java xxx.class时,SxtStu类的相关信息都会被加载到方法区:

代码,静态变量,静态方法(也包括了main方法),字符串常量(“高琪”、“联想”、“我正在学习!使用我们的电脑,”)

接着虚拟机开始寻找main方法,main方法也会被放到栈中的一个栈帧里面,从main方法开始执行代码

首先,栈帧里面先定义了一个对象 stu,因为没有定义,而且是一个引用类型,所以他的值为null。

然后,接着运行new SxtStu()方法,在栈里面有一个新的栈帧,用来存放new 方法。

另外,在堆内存里面,生成了一个新的空间(有着自己的地址),里面是包括了类SxtStu的属性:id,sname,age,comp,和方法study(),play()。一开始,基本数据类型的值是0,引用类型的值是null

对象创建完成,构造方法 SxtStu() 的栈帧就会消失

接下来, = 把生成的堆的地址传给stu对象,stu的对象的值不再为null,而是堆产生的对象的地址。

此后,通过对象.属性的方式给对应的属性赋值。sname的值是字符串,因此将方法区中,字符串常量“张三”的地址赋给sname。

之后,用同样的方法,虚拟机创建了comp1对象,并且成功实例化,在堆中开辟了一块用于存放Computer类型的对象的一块空间,并将地址赋给了栈中的comp1对象。

接下来, stu1.comp = comp1; 将堆中的对象的stu1对应的属性comp的值设为堆中的new出来的Computer类型的对象的地址。

随后执行stu1对象的study()方法。打印出相关的相关的字符串以及变量的值。

左边方法区完成后将构造方法(笔误,前面已经删除),main方法从栈中删除,main方法删除后,堆里面存放的相关数据也全部都消失。

至此,面向对象的整个内存分析结束!

构造方法

构造器也叫构造方法(constructor),用于对象的初始化。构造器是一个创建对象时被自动调用的特殊方法,目的是对象的初始化

构造器的名称应与类的名称一致。

Java通过new关键字来调用构造器,从而返回该类的实例,是一种特殊的方法

声明格式:

[修饰符] 类名(形参列表){
    //n条语句
}

要点:

  1. 通过new关键字调用!!

  2. 构造器虽然有返回值,但是不能定义返回值类型(返回值的类型肯定是本类),不能在构造器里使用return返回某个值。

    可以写return;但是没有必要,写不写都是返回一个对象。

  3. 如果我们没有定义构造器,则编译器会自动定义一个无参的构造函数。如果已定义则编译器不会自动添加!

  4. 构造器的方法名必须和类名一致!

初始化成员变量后可以通过下列方法快速创建构造方法,get,set,toString方法等。
在这里插入图片描述

构造方法也叫构造器(constructor),用于对象的初始化

使用构造方法:

Stu 追 = new Stu(100, "上来", 21);

即可对Stu对象“追”的id,sname,age进行初始化。

前提:Stu类要有自己定义的构造方法:

public Stu(int id,String name,int age) {
		//super();//构造方法的第一句总是super();没写编译器会自动加,不写也没有关系
		this.id=id;		//this表示创建好的对象
		this.sname=name;
		this.age=age;
	}

如果没有自己定义的构造方法,那么会使用系统自定义的构造方法:

public Stu() {
		
	}

并要对对象的属性进行一个个的赋值,相当麻烦。这就体现了构造方法的便利之处。

Stu  stu = new Stu();   
stu.id=1001;
stu.sname= "靓仔";
stu.age = 21;
//如果自己定义了构造方法,则原有的构造方法:Stu()会被覆盖,这时需要自己显式地写出系统定义的无参构造方法(上面那个),通过重载的无参构造方法创建对象。

注意:如果你没有定义构造方法,编译器会自动给你加一个无参的,

但是当你定义了有参的构造方法时,就不会给你自己加一个了,这时候如果你只是new(有参数)这么用没有问题,如果你用到了new ()那么不好意思,因为这个不存在。所以这时候需要自己写一个无参的。

尤其在使用框架时,我们更要如此,不然很容易会发生错误。

Stu  stu = new Stu();    //报错,说类型不匹配		

//易错点:
public class MyClass {
    long var;
//    这里比构造函数多了返回值:void,所以不是构造函数
    public void MyClass(long param){
        var = param;
    }

    public static void main(String[] args) {
        MyClass a,b;
        a = new MyClass();//可以成功,因为上面的不是构造函数
        System.out.println();
        b = new MyClass(5);//失败,这个类只有一个构造函数,那就是默认的无参构造函数
//      'MyClass()' cannot be applied to '(int)'
    }
}

构造方法的重载

构造方法也是方法,只不过有特殊的作用而已。与普通方法一样,构造方法也可以重载。

我们经常需要对构造方法进行重载:构造方法名相同,形参列表(参数个数,类型,顺序)不同。

构造方法重载(创建不同用户对象)

public class User {
    int id; // id
    String name; // 账户名
    String pwd; // 密码
    public User() {
 
    }
    public User(int id, String name) {
        super();
        this.id = id;  //在这里,this指的是“较远”的那个属性,即成员变量;不加this则是局部变量
        this.name = name;
    }
    public User(int id, String name, String pwd) {
        this.id = id;
        this.name = name;
        this.pwd = pwd;
    }
    public static void main(String[] args) {
        User u1 = new User();
        User u2 = new User(101, "高小七");
        User u3 = new User(100, "高淇", "123456");     
    }
}

垃圾回收机制(Garbage Collection)

Java引入了垃圾回收机制,令C++程序员最头疼的内存管理问题迎刃而解。Java程序员可以将更多的精力放到业务逻辑上而不是内存管理工作上,大大的提高了开发效率。

垃圾回收原理和算法

内存管理

Java的内存管理很大程度指的就是对象的管理,其中包括对象空间的分配和释放

对象空间的分配:使用new关键字创建对象即可

对象空间的释放:将对象赋值null即可。垃圾回收器将负责回收所有”不可达”对象的内存空间。

垃圾回收过程

任何一种垃圾回收算法一般要做两件基本事情:

  1. 发现无用的对象(重点)
  2. 回收无用对象占用的内存空间。

垃圾回收机制保证可以将“无用的对象”进行回收。无用的对象指的就是没有任何变量引用该对象。Java的垃圾回收器通过相关算法发现无用对象,并进行清除和整理。

垃圾回收相关算法

  1. 引用计数法

堆中每个对象都有一个引用计数。被引用一次,计数加1. 被引用变量值变为null,则计数减1,直到计数为0,则表示变成无用对象。优点是算法简单,缺点是“循环引用的无用对象”无法别识别。

//循环引用示例  
public class Student {
    String name;
    Student friend;
     
    public static void main(String[] args) {
        Student s1 = new Student();
        Student s2 = new Student();
         
        s1.friend = s2;
        s2.friend = s1;        
        s1 = null;
        s2 = null;
    }
}

s1和s2互相引用对方,导致他们引用计数不为0,但是实际已经无用,但无法被识别。

  1. 引用可达法(根搜索算法)

程序把所有的引用关系看作一张,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点

通用的分代垃圾回收机制

分代垃圾回收机制,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。我们将对象分为三种状态:年轻代年老代持久代。JVM将堆内存划分为 Eden(伊甸园,指的是最开始的状态)、SurvivorTenured/Old 空间。

  1. 年轻代

    年轻代分为Eden区两个Survivor(一般为两个,也可多个)

    所有新生成的对象首先都是放在Eden区。 年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象,对应的是Minor GC,每次 Minor GC 会清理年轻代的内存,算法采用效率较高的复制算法,频繁的操作,但是会浪费内存空间。当“年轻代”区域存放满对象后,就将对象存放到年老代区域。

  2. 年老代

Tenured/Old

在年轻代中经历了N(默认15)次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。年老代对象越来越多,我们就需要启动Major GCFull GC(全量回收),来一次大扫除,全面清理年轻代区域和年老代区域。

  1. 持久代

方法区

用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响。
在这里插入图片描述

Minor GC:

用于清理年轻代区域。Eden区满了就会触发一次Minor GC。清理无用对象,将有用对象复制到“Survivor1”、“Survivor2”区中(这两个区,大小空间也相同,同一时刻Survivor1和Survivor2只有一个在用,一个为空)

Major GC

用于清理老年代区域。

Full GC

用于清理年轻代、年老代区域。 成本较高,会对系统性能产生影响。

垃圾回收过程:

1、新创建的对象,绝大多数都会存储在Eden中,

2、当Eden满了(或达到一定比例)不能创建新对象,则触发垃圾回收(GC),将无用对象清理掉,

​ 然后剩余对象复制到某个Survivor中,如S1,同时清空Eden区

3、当Eden区再次满了,会将S1中的不能清空的对象存到另外一个Survivor中(意思就是说原本S1中的一些对象也 会被回收),如S2,

​ 同时将Eden区中的不能清空的对象,也复制到S1中,保证Eden和S1,均被清空。

4、Eden区第三次满了,出发Minor GC, 把Eden和Survivor2中有用的,复制到Survivor1, 同时清空Eden,Survivor2。

​ 形成循环,Survoivor1和Survivor中来回清空、复制,过程中有一个Survivor处于空的状态用于下次复制的。

5、重复多次(默认15次),Survivor中没有被清理的对象,则会复制到老年代Old(Tenured)区中,

6、当Old区达到一定比例,触发Major GC,清理老年代。

7、当Old满了,则会触发一个一次完整地垃圾回收(FullGC)。注意,Full GC清理代价大,系统资源消耗高。

JVM调优和Full GC

在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:

1.年老代(Tenured)被写满

2.持久代(Perm)被写满

3.System.gc()被显式调用(程序建议GC启动,不是调用GC

4.上一次GC之后Heap的各域分配策略动态变化

开发中容易造成内存泄露的操作

如下四种情况时最容易造成内存泄露的场景,请大家开发时一定注意:

创建大量无用对象

比如,我们在需要大量拼接字符串时,使用了String而不是StringBuilder()。

String str = "";
for (int i = 0; i < 10000; i++) { 
     str += i;     //相当于产生了10000个String对象
}

静态集合类的使用

HashMap、Vector、List等的使用最容易出现内存泄露(为什么???),这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放。

各种连接对象(IO流对象、数据库连接对象、网络连接对象)未关闭

IO流对象、数据库连接对象、网络连接对象等连接对象属于物理连接,和硬盘或者网络连接,不使用的时候一定要关闭。

监听器的使用

释放对象时,没有删除相应的监听器。

要点:

  1. 程序员无权调用垃圾回收器
  2. 程序员可以调用System.gc(),该方法只是通知JVM,并不是运行垃圾回收器。尽量少用,会申请启动Full GC,成本高,影响系统性能。
  3. finalize方法,是Java提供给程序员用来释放对象或资源的方法,但是尽量少用

this关键字

对象创建的过程和this的本质

构造方法是创建Java对象的重要途径,通过new关键字调用构造器时,构造器也确实返回该类的对象,但这个对象并不是完全由构造器负责创建。创建一个对象分为如下四步:

  1. 分配对象空间,并将对象成员变量初始化为0或空
  2. 执行属性值的显式初始化
  3. 执行构造方法
  4. 返回对象的地址给相关的变量

this的本质就是“创建好的对象的地址”! 由于在构造方法调用前,对象已经创建。因此,在构造方法中也可以使用this代表“当前对象” 。

this最常的用法:

  1. 在程序中产生二义性之处,应使用this来指明当前对象

    普通方法中,this总是指向调用该方法的对象

    构造方法中,this总是指向正要初始化的对象

  2. 使用this关键字调用重载的构造方法,避免相同的初始化代码。但只能在构造方法中用,并且必须位于构造方法的第一句

  3. this不能用于static方法中

原因如下:this指的是当前对象,static修饰的静态方法是都在方法区,方法区里面放的是类信息,没有对象,不能通过this来指定对象。-----来自尚学堂视频讲解。

static方法区中为什么不能有this或者super?

想象一下Java中为什么main方法是程序的入口?
public static void main(String[] args)
因为main方法是静态方法,其实带静态的东西优先级都比较高,静态代码块,静态变量,静态常量等等。。。
静态方法也是一样,那么为什么main方法是程序的入口呢?因为他最先被执行啊!

当你点击编译按钮时,也就是类加载时静态方法就被加载到了内存区,静态方法被优先执行,而此时对象都没被加载呢。this是当前类的对象,可想而知在静态方法执行的时候它还不存在呢,因此在静态方法中通过this调用其他任何东西都是扯淡。所以当然不能在静态方法区中使用this,super也是同理

this代表“当前对象”示例

public class User {
    int id;        //id
    String name;   //账户名
    String pwd;   //密码

    public User() {
    }
    public User(int id, String name) {
        System.out.println("正在初始化已经创建好的对象:"+this);
        this.id = id;   //不写this,无法区分局部变量id和成员变量id
        this.name = name;
    }
    public void login(){
        System.out.println(this.name+",要登录!");  //不写this效果一样:没有和其混淆的属性名
    }

    public static void main(String[] args) {
        User  u3 = new User(101,"高小七");
        System.out.println("打印高小七对象:"+u3);
        u3.login();
    }
}
结果:
正在初始化已经创建好的对象:User@3e3abc88
打印高小七对象:User@3e3abc88
高小七,要登录!

this()调用重载构造方法

public class TestThis {
    int a, b, c;

    TestThis() {
        System.out.println("正要初始化一个Hello对象");
    }
    TestThis(int a, int b) {
        // TestThis(); //这样是无法调用构造方法的!
        this(); // 调用无参的构造方法,并且必须位于第一行!
        //a = a;// 这里都是指的局部变量而不是成员变量,无法给属性赋值
// 这样就区分了成员变量和局部变量. 这种情况占了this使用情况大多数!
        this.a = a;
        this.b = b;
    }
    TestThis(int a, int b, int c) {
        this(a, b); // 调用带参的构造方法,并且必须位于第一行!
        this.c = c;
    }

    void sing() {
        System.out.println("唱歌");
    }
    void eat() {
        //this.sing(); // 调用本类中的sing();
        sing(); // 调用本类中的sing(),因为没有让人产生混淆的其他方法,所以可以不用this;
        System.out.println("你妈妈喊你回家吃饭!");
    }

    public static void main(String[] args) {
        TestThis hi = new TestThis(2, 3);
        hi.eat();
    }
}
结果:
正要初始化一个Hello对象
唱歌
你妈妈喊你回家吃饭!

static 关键字

在类中,用static声明的成员变量为静态成员变量,也称为类变量。 类变量的生命周期和类相同,在整个应用程序执行期间都有效。它有如下特点:

  1. 为该类的公用变量,属于类,被该类的所有实例共享,在类被载入时被显式初始化。
  2. 对于该类的所有对象来说,static成员变量只有一份。被该类的所有对象共享!!
  3. 一般用“类名.类属性/方法”来调用。(也可以通过对象引用或类名(不需要实例化)访问静态成员。)
  4. 在static方法中不可直接访问非static的成员。:防止出现其他非static成员没有创建,就访问该成员造成的空指针异常,道理同static里面不可以用super和this两个关键字。

核心要点:

static修饰的成员变量和方法,从属于类。

普通变量和方法从属于对象的。

static关键字的使用

public class User2 {
    int id; // id
    String name; // 账户名
    String pwd; // 密码

    static String company = "北京尚学堂"; // 公司名称,静态变量,被所有对象共享


    public User2(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public void login() {
        printCompany();
        System.out.println(company);
        System.out.println("登录:" + name);
    }

    public static void printCompany() {
//      login();//在静态方法中调用非静态成员,编译就会报错
        System.out.println(company);//在静态方法中只能调用静态变量
    }

    public static void main(String[] args) {
        User2 u = new User2(101, "高小七");
        User2.printCompany(); //北京尚学堂
        User2.company = "北京阿里爷爷";
        User2.printCompany(); //北京阿里爷爷
        u.printCompany(); //北京阿里爷爷 ,一旦全局变量被修改,那么所有的对象使用的该静态变量也会被改
    }
}

在这里插入图片描述

成员变量与静态变量

如果给成员变量和成员方法加上static,就会变成静态变量和静态方法。

成员方法和成员变量又称为实例方法和实例变量。

静态方法和静态变量又称为方法和类变量。

静态变量,静态方法不仅可以通过对象进行访问,还可以通过类名直接访问

成员方法不仅可以操作成员变量,成员方法,还可以操作静态变量,静态方法。

静态方法只能操作静态变量,静态方法,这是因为在类创建对象之前,实例成员变量还没有分配。

如果一个方法不需要操作成员变量,就可以将这样的方法声明为静态方法,避免创建对象浪费内存

静态初始化块

构造方法用于对象的初始化!

静态初始化块,用于类的初始化操作!在静态初始化块中不能直接访问非static成员。

注意事项:

静态初始化块执行顺序(学完继承再看这里):

  1. 上溯到Object类,先执行Object的静态初始化块,再向下执行子类的静态初始化块,直到我们的类的静态初始化块为止。(即先执行高层的静态初始化块)
  2. 构造方法执行顺序和上面顺序一样!!

若类有构造方法的话,先静态语句块,再构造方法。

static初始化块

public class User3 {
    int id;        //id
    String name;   //账户名
    String pwd;   //密码
    static String company; //公司名称
    static {
        System.out.println("执行类的初始化工作");
        company = "北京尚学堂";
        printCompany();
    }
    public static void printCompany(){
        System.out.println(company);
    }
    public static void main(String[] args) {
       // User3  u3 = new User3();
        User3  u3; //当类初始化时就会调用到静态初始化块
    }
}
结果:
执行类的初始化工作
北京尚学堂

参数传值机制

Java中,方法中所有参数都是“值传递”,也就是“传递的是值的副本”。 也就是说,我们得到的是“原参数的复印件,而不是原件”。因此,复印件改变不会影响原件。

基本数据类型参数的传值

传递的是值的副本。 副本改变不会影响原件。

引用类型参数的传值

传递的是值的副本。但是引用类型指的是“对象的地址”。

因此,副本和原参数都指向了同一个“地址”

改变“副本“指向地址对象的值,也意味着原参数指向对象的值也发生了改变。—所以说如果

向参数传递的值的级别不可以高于该参数的级别,例如不可以向int类型的参数传递一个float值,但可以向double型的参数传递一个float值。

引用型数据包括对象,数组,接口。此时传值传的是变量中存放的引用(对象等的地址),而不是变量所引用的实体

public class User4 {
    int id;        //id
    String name;   //账户名
    String pwd;   //密码

    public User4(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public   void   testParameterTransfer01(User4  u){
        u.name="高小八";
    }

    public   void   testParameterTransfer02(User4  u){
        u  =  new  User4(200,"高三");
    }

    public static void main(String[] args) {
        User4   u1  =  new User4(100, "高小七");

        u1.testParameterTransfer01(u1);
        System.out.println(u1.name); //高小八

        u1.testParameterTransfer02(u1);
        System.out.println(u1.name); //高小八
    }
}

在这里插入图片描述
过程解析:

刚开始,对象u1的id是100,name是“高小七”,对象的地址是123.

随后,u1调用了testParameterTransfer01(u1);将u1的地址123传给了成员变量:u;u此时的地址也变为了123.

之后,u将该地址下的name改为“高小八”,修改成功。

随后,u1调用了testParameterTransfer02(u1);将u1的地址123传给了成员变量:u;u此时的地址也变为了123.

但是,testParameterTransfer02(u1)又创建了一个新的对象,id是200,name是“高三”,对象的地址是124. 然后新的对象地址124传给了成员变量u, u指向的地址由“123”变成了“124”,不再对123这个内存里面的数值进行改变,所以修改失败。

随后方法调用结束,地址124指向的内存被回收。

包机制是Java中管理类的重要手段。 开发中,我们会遇到大量同名的类

通过包我们很容易对解决类重名的问题,也可以实现对类的有效管理。 包对于类,相当于文件夹对于文件的作用。

package

我们通过package实现对类的管理,package的使用有两个要点:

  1. 通常是类的第一句非注释性语句。
  2. 包名:域名倒着写即可,再加上模块名,便于内部管理类。

package的命名举例

com.公司名.系统名
com.sun.test;
com.oracle.test;
cn.sxt.gao.test;
cn.sxt.gao.view;
cn.sxt.gao.view.model;

注意事项:

  1. 写项目时都要加包,不要使用默认包
  2. com.gao和com.gao.car,这两个包没有包含关系,是两个完全独立的包。(事实上就是包含关系,这要怎么理解?)只是逻辑上看起来后者是前者的一部分。

JDK中的主要包

Java中的常用包说明
java.lang包含一些Java语言的核心类,如String、Math、Integer、System和Thread,提供常用功能。
java.awt包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)。
java.net包含执行与网络相关的操作的类。
java.io包含能提供多种输入/输出功能的类。
java.util包含一些实用工具类,如定义系统特性、使用与日期日历相关的函数。

java.lang包(lang-language)不用导入,可以直接使用它的类

导入类import

如果我们要使用其他包的类,需要使用import导入,从而可以在本类中直接通过类名来调用,否则就需要书写类的完整包名和类名。import后,便于编写代码,提高可维护性。

注意要点:

  1. Java会默认导入java.lang包下所有的类,因此这些类我们可以直接使用。
  2. 如果导入两个同名的类,只能用包名+类名来显示调用相关类:
java.util.Date date  = new  java.util.Date();

3.用 * 可以默认导入某个包下所有的其他包,会降低编译速度,但不会降低运行速度。

导入同名类的处理

//当不同的包有着相同的类时,系统会优先选择包名较为精确的:java.util.Date
//更加推荐的方法时在类名前面加上包名(最古老的方法),使可读性更强。

import java.sql.Date;
import java.util.*;//导入该包下所有的类。会降低编译速度,但不会降低运行速度。
 
public class Test{
    public static void main(String[] args) {
        //这里指的是java.sql.Date
        Date now; 
        //java.util.Date因为和java.sql.Date类同名,需要完整路径
        java.util.Date  now2 = new java.util.Date();
        System.out.println(now2);      
        //java.util包的非同名类不需要完整路径
        Scanner input = new Scanner(System.in);    
    }
}

静态导入

静态导入(static import)是在JDK1.5新增加的功能,其作用是用于导入指定类的静态属性,这样我们可以直接使用静态属性。

静态导入的使用

package cn.sxt;
 //以下两种静态导入的方式二选一即可
import static java.lang.Math.*;//导入Math类的所有静态属性
import static java.lang.Math.PI;//导入Math类的PI属性
 
public class Test2{
    public static void main(String [] args){
        System.out.println(PI);
        System.out.println(random());
    }
}
//尽管lang包无需导入,但是如果没有静态导入PI,那么就不可以使用PI。

概述

面向对象的三大特征:继承封装多态

继承的实现

继承,extends,也有扩展的意思。子类继承父类,即子类是父类的扩展。

继承使用要点:

1.父类也称作超类、基类、派生类等。

2.Java中只有单继承,没有像C++那样的多继承。多继承会引起混乱,使得继承链过于复杂,系统难于维护。

3.Java中类没有多继承,接口有多继承

4.子类继承父类,可以得到父类的全部属性和方法 (除了父类的构造方法),但不见得可以直接访问(比如,父类私有的属性和方法)。

5.如果定义一个类时,没有调用extends,则它的父类是:java.lang.Object

在idea中,我们可以右键,选择Diagrams,再show Diagrams查看类的继承关系。

instanceof 运算符

instanceof是二元运算符,左边是对象,右边是类;当对象是右面类或子类所创建对象时,返回true;否则,返回false。比如:

//使用instanceof运算符进行类型判断
public class Test{
    public static void main(String[] args) {
        Student s = new Student("高淇",172,"Java");
        System.out.println(s instanceof Person);  
        System.out.println(s instanceof Student);
    }
}
结果:
true
true

方法的重写override

子类通过重写父类的方法,可以用自身的行为替换父类的行为。方法的重写是实现多态的必要条件

方法的重写需要符合下面的三个要点:

1.“==”: 方法名、形参列表相同。

2.“≤”:返回值类型声明异常类型,子类小于等于父类。

是指当类返回一个对象时,子类返回值必须是父类返回值或者是父类返回值的子类

3.“≥”: 访问权限,子类大于等于父类。(之前不是特别了解)

public class TestOverride {
    public static void main(String[] args) {
        Vehicle v1 = new Vehicle();
        Vehicle v2 = new Horse();
        Vehicle v3 = new Plane();
        v1.run();
        v2.run();
        v3.run();
        v2.stop();
        v3.stop();
    结果:
        跑....
		四蹄翻飞,嘚嘚嘚...
		天上飞!
		停止不动
		空中不能停,坠毁了!
    }
}

class Vehicle { // 交通工具类
    public void run() {
        System.out.println("跑....");
    }
    public void stop() {
        System.out.println("停止不动");
    }
}
class Horse extends Vehicle { // 马也是交通工具
    public void run() { // 重写父类方法
        System.out.println("四蹄翻飞,嘚嘚嘚...");
    }
}

class Plane extends Vehicle {
    public void run() { // 重写父类方法
        System.out.println("天上飞!");
    }
    public void stop() {
        System.out.println("空中不能停,坠毁了!");
    }
}
//子类返回值必须是父类返回值或者是父类返回值的子类
public class Person {
}
public class Student extends Person{
}
public class TestOverride2 {
    class Vehicle{
        public Person whoIsPsg(){
            return new Person();
        }
    }
    class Horse extends Vehicle{
        public Student whoIsPsg(){
            return new Student(); //student比person小,可以。如果student换成object就不行。
        }
    }
}

Object类基本特性

Object类是所有Java类的根基类,也就意味着所有的Java对象都拥有Object类的属性和方法。如果在类的声明中未使用extends关键字指明其父类,则默认继承Object类。

public class Person {
    ...
}
//等价于:
public class Person extends Object {
    ...
}

在这里插入图片描述
native表示不是用java实现,而是本地实现,需要调用本地操作系统里面的内容。

toString方法

Object类中定义有public String toString()方法,其返回值是 String 类型

Object类中toString方法的源码为:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

根据如上源码得知,默认会返回“类名+@+16进制的hashcode”。(hashcode可以暂时理解为一个地址。)

在打印输出或者用字符串连接对象时,会自动调用该对象的toString()方法。

System.out.println(xxx);,实际上是
System.out.println(xxx.toString();

class Person {
    String name;
    int age;
    //重写toString()方法
    @Override
    public String toString() {
        return name+",年龄:"+age;
    }
}
public class Test {
    public static void main(String[] args) {
        Person p=new Person();
        p.age=20;
        p.name="李东";
        System.out.println("info:"+p);

        Test t = new Test();
        System.out.println(t);
    }
}
结果:
info:李东,年龄:20
Test@3e3abc88

==和equals方法

==

“==”代表比较双方是否相同。

如果是基本类型则表示值相等,如果是引用类型则表示地址相等即是同一个对象。

Object

object内equals方法的源码:说明默认的功能和==一样。
在这里插入图片描述

Object类中定义有:public boolean equals(Object obj)方法,提供定义“对象内容相等”的逻辑。比如,我们在公安系统中认为id相同的人就是同一个人、学籍系统中认为学号相同的人就是同一个人。

Object 的 equals 方法默认就是比较两个对象的hashcode,是同一个对象的引用时返回 true 否则返回 false。但是,我们可以根据我们自己的要求重写equals方法

类的hashCode方法和equals方法都可以重写,返回的值完全在于自己定义。

所以equals为true,hashCode不一定相同,反之亦然。

hashCode()返回该对象的哈希码值;equals()返回两个对象是否相等。

关于hashCode和equal是方法是有一些 常规协定 :

1、两个对象用equals()比较返回true,那么两个对象的hashCode()方法必须返回相同的结果。

2、两个对象用equals()比较返回false,不要求hashCode()方法也一定返回不同的值,但是最好返回不同值,亿提搞哈希表性能。

3、重写equals()方法,必须重写hashCode()方法,以保证equals方法相等时两个对象hashcode返回相同的值。

基本类型引用类型
==值相等地址相等
equals用于对象的比较比较对象的hashcode
public class TestEquals {
    public static void main(String[] args) {
        Person p1 = new Person(123,"高淇");
        Person p2 = new Person(123,"高小七");
        System.out.println(p1==p2);     //false,不是同一个对象,对象地址不同
        System.out.println(p1.equals(p2));  //true,id相同则认为两个对象内容相同,因为重写了equals()方法
        String s1 = new String("尚学堂");
        String s2 = new String("尚学堂");
        System.out.println(s1==s2);         //false, 两个字符串不是同一个对象,对象地址不同
        System.out.println(s1.equals(s2));  //true,  两个字符串内容相同
       //String类内部也有重写equals方法,使得字符串通过逐个比较字符来判断是否相等。(重点,要好好理解!)
    }
}
class Person {
    int id;
    String name;
    public Person(int id,String name) {
        this.id=id;
        this.name=name;
    }
    public boolean equals(Object obj) {
        if(obj == null){
            return false;
        }else {
            if(obj instanceof Person) {
                Person c = (Person)obj;
                if(c.id==this.id) {
                    return true;
                }
            }
        }
        return false;
    }
}

JDK提供的一些类,如String、Date、包装类等,重写了Object的equals方法,调用这些类的equals方法, x.equals (y) ,当x和y所引用的对象是同一类对象属性内容相等时(也有可能并不一定是相同对象),返回 true 否则返回 false。

我们自己的类也可以重写equals方法和hashCode()方法,并且可以通过编译器自动生成。

super关键字

super是直接父类对象的引用。可以通过super来访问父类中被子类覆盖的方法或属性

使用super调用普通方法,语句没有位置限制,可以在子类中随便调用。

若是构造方法的第一行代码没有显式的调用super(…)或者this(…);那么Java默认都会调用super(),含义是调用父类的无参数构造方法。这里的super()可以省略

public class TestSuper01 {
    public static void main(String[] args) {
        new ChildClass().f();
    }
}
class FatherClass {
    public int value;
    public void f(){
        value = 100;
        System.out.println ("FatherClass.value="+value);
    }
}
class ChildClass extends FatherClass {
    public int value;
    public void f() {
        super.f();  //调用父类对象的普通方法,没有位置限制
        value = 200;
        System.out.println("ChildClass.value="+value);
        System.out.println(value);
        System.out.println(super.value); //调用父类对象的成员变量
    }
}
结果:
FatherClass.value=100
ChildClass.value=200
200
100

继承树追溯

属性/方法查找顺序:(比如:查找变量h)

  1. 查找当前类中有没有属性h
  2. 依次上溯每个父类,查看每个父类中是否有h,直到Object
  3. 如果没找到,则出现编译错误。
  4. 上面步骤,只要找到h变量,则这个过程终止。

从下往上找。

构造方法调用顺序:

构造方法第一句总是:super(…)来调用父类对应的构造方法。所以,流程就是:先向上追溯到Object,然后再依次向下执行类的初始化块和构造方法,直到当前子类为止。

注:静态初始化块调用顺序,与构造方法调用顺序一样,不再重复。

有静态语句块的话,先静态语句块,再构造方法。

public class TestSuper02 { 
    public static void main(String[] args) {
        System.out.println("开始创建一个ChildClass对象......");
        new ChildClass();
    }
}
class FatherClass {
    public FatherClass() {
        System.out.println("创建FatherClass");
    }
}
class ChildClass extends FatherClass {
    public ChildClass() {
        System.out.println("创建ChildClass");
    }
}
结果:
开始创建一个ChildClass对象......
创建FatherClass
创建ChildClass

子类的构造方法第一句就是super(),没有写的话默认有;即一开始就会调用父类的构造方法。

所以在例子中时:先构造Object,再构造FatherClass2,最后构造ChildClass2.

封装

封装的作用和含义

需要让用户知道的才暴露出来,不需要让用户知道的全部隐藏起来,这就是封装。说的专业一点,封装就是把对象的属性和操作结合为一个独立的整体,并尽可能隐藏对象的内部实现细节

我们程序设计要追求“高内聚,低耦合”。

高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合是仅暴露少量的方法给外部使用,尽量方便外部调用。

编程中封装的具体优点:

  1. 提高代码的安全性。
  2. 提高代码的复用性。
  3. “高内聚”:封装细节,便于修改内部代码,提高可维护性。
  4. “低耦合”:简化外部调用,便于调用者使用,便于扩展和协作。

没有封装的代码会出现一些问题

class Person {
    String name;
    int age;
    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}
public class Test {
    public static void main(String[] args) {
        Person p = new Person();
        p.name = "小红";
        p.age = -45;//年龄可以通过这种方式随意赋值,没有任何限制
        System.out.println(p);
    }
}

再比如说,如果哪天我们需要将Person类中的age属性修改为String类型的,你会怎么办?你只有一处使用了这个类的话那还比较幸运,但如果你有几十处甚至上百处都用到了,那你岂不是要改到崩溃。而封装恰恰能解决这样的问题。如果使用封装,我们只需要稍微修改下Person类的setAge()方法即可,而无需修改使用了该类的客户代码。

这个不清楚,到底要怎么操作?

封装的实现—使用访问控制符

Java是使用“访问控制符”来控制哪些细节需要封装,哪些细节需要暴露的。

Java中4种“访问控制符”分别为private、default、protected、public,它们说明了面向对象的封装性,所以我们要利用它们尽可能的让访问权限降到最低,从而提高安全性。

记忆: 类		包		子		所
	private default protected public

在这里插入图片描述

  1. private 表示私有,只有自己类能访问
  2. default表示没有修饰符修饰,只有同一个包的类能访问(如果类没有显式的包名,那么就是在默认的包下面,包下其他的类就可以访问)
  3. protected表示可以被同一个包的类以及其他包中的子类访问(这里要注意protected修饰的类可以被同一个包的类访问)
  4. public表示可以被该项目的所有包中的所有类访问

访问控制符可以用来修饰类、属性、方法

一个类 使用 不同的包的类中的protected或public修饰的属性要先把包(*包.包.类名,要记得类名一定要加进来)import进来。

protected修饰的属性,方法不能被其他包中的类使用,其他包可以通过创建包含protected的子类,创建的子类的实例化的对象就可以来使用protected修饰的属性和方法。

//父类在包 test1下
package com.atguigu.test1;

public class Father {
    private String name = "Tom";
    //该方法是 protected 访问权限
    protected String getName(){
        return this.name;
    }
}
//子类在另一个包 test2下
package com.atguigu.test2;

import com.atguigu.test1.Father;

public class Son extends Father{
    public static void main(String[] args) {
        Father f = new Father();
		//String name1 = f.getName();  失败,用protected修饰的方法,f不是子类,不在同一个包
        //'getName()' has protected access in 'com.atguigu.test1.Father'
        Son s = new Son();
  String name2 = s.getName();//成功,因为类Son是类Father的子类,所以可以使用被protected修饰的方法
    }
}
//子类和父类在同一个包 test1下
package com.atguigu.test1;

public class Son extends Father{
    public static void main(String[] args) {
        Father f = new Father();
        String name = f.getName(); //这个Son类和Father类属于同一个包,所以可以用
        Son s = new Son();
        String name2 = s.getName(); //子类,当然可以使用父类的方法
    }
}

封装的使用细节

类的属性的处理:

  1. 一般使用private访问权限。
  2. 提供相应的get/set方法来访问相关属性,这些方法通常是public修饰的,以提供对属性的赋值与读取操作(注意:boolean变量的get方法是is开头!)。
  3. 一些只用于本类的辅助性方法可以用private修饰,希望其他类调用的方法用public修饰。

像是一些简单的方法,也一般是采用public来修饰。

public class Person {
    // 属性一般使用private修饰
    private String name;
    private int age;
    private boolean flag;
    // 为属性提供public修饰的set/get方法
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {java
        this.age = age;
    }
    public boolean isFlag() {// 注意:boolean类型的属性get方法是is开头的
        return flag;
    }
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

通过set设置属性值可以通过在set方法里面加限定条件,这比直接通过对象.属性名来设置属性值要好很多。

下面我们使用封装来解决一下年龄非法赋值的问题

class Person {
    private String name;
    private int age;
    public Person() {
 
    }
    public Person(String name, int age) {
        this.name = name;
        // this.age = age;//在这里,构造方法中也不能直接赋值,应该调用setAge方法
        //其实可以用,但是不规范.因为此时一旦不用setAge(Age),就不能对age进行限制
        setAge(age);
    }
     
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setAge(int age) {
        //在赋值之前先判断年龄是否合法
        if (age > 130 || age < 0) {
            this.age = 18;//不合法赋默认值18
        } else {
            this.age = age;//合法才能赋值给属性age
        }
    }
    public int getAge() {
        return age;
    }
    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}
 
public class Test2 {
    public static void main(String[] args) {
        Person p1 = new Person();
        //p1.name = "小红"; //编译错误
        //p1.age = -45;  //编译错误
        p1.setName("小红");
        p1.setAge(-45);  //18
        System.out.println(p1);
         
        Person p2 = new Person("小白", 300);
        System.out.println(p2);  //18
    }
}

多态

多态指的是同一个方法调用,由于对象不同可能会有不同的行为。现实生活中,同一个方法,具体实现会完全不同。 比如:同样是调用人的“休息”方法,张三是睡觉,李四是旅游,高淇老师是敲代码,数学教授是做数学题; 同样是调用人“吃饭”的方法,中国人用筷子吃饭,英国人用刀叉吃饭,印度人用手吃饭。

多态的要点:

  1. 多态是方法的多态,不是属性的多态(多态与属性无关)。
  2. 多态的存在要有3个必要条件:继承方法重写父类引用指向子类对象(最后一点很重要)。
  3. 父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了。

多态和类型转换测试

class Animal {
    public void shout() {
        System.out.println("叫了一声!");
    }
}
class Dog extends Animal {
    public void shout() {
        System.out.println("旺旺旺!");
    }
    public void seeDoor() {
        System.out.println("看门中....");
    }
}
class Cat extends Animal {
    public void shout() {
        System.out.println("喵喵喵喵!");
    }
}
public class TestPolym {
    public static void main(String[] args) {
        Animal a1 = new Cat(); // 向上可以自动转型
        //传的具体是哪一个子类就调用哪一个子类的方法。大大提高了程序的可扩展性。
        animalCry(a1);
        Animal a2 = new Dog();
        animalCry(a2);//a2为编译类型,Dog对象才是运行时类型。
         
        //编写程序时,如果想调用运行时类型的方法,只能进行强制类型转换。
        // 否则通不过编译器的检查。
        Dog dog = (Dog)a2;//向下需要强制类型转换
        dog.seeDoor();
    }
 
    // 有了多态,只需要让增加的这个类继承Animal类就可以了。
    static void animalCry(Animal a) {
        a.shout();
    }
    
    结果:
        喵喵喵喵!
        旺旺旺!
        看门中....
 
    /* 如果没有多态,我们这里需要写很多重载的方法。
     * 每增加一种动物,就需要重载一种动物的喊叫方法。非常麻烦。
    static void animalCry(Dog d) {
        d.shout();
    }
    static void animalCry(Cat c) {
        c.shout();
    }*/
}
子类Cat继承父类Animal,并且重写了父类的方法shout();

重点!这段话讲的很好,很能方便理解。
创建的是父类的引用,开辟的是子类的空间,由于没有开辟父类的空间,所以是父类的引用指向子类对象,并不是真正的父类自己创建了对象。
而且父类对象的引用还可以作为函数参数来接收子类对象哦!

父类引用指向子类对象:用父类创建子类对象:Animal a1=new Cat() ,使用方法时传入的也是子类对象:aniamlCry(a1),子类在方法里面使用的是自己重写过的方法:a.shout()。(最后一点要多看多理解)

示例给大家展示了多态最为多见的一种用法,即父类引用方法的形参实参可以是任意的子类对象,可以通过不同的子类对象实现不同的行为方式。

由此,我们可以看出多态的主要优势是提高了代码的可扩展性,符合开闭原则

但是多态也有弊端,就是无法调用子类特有的功能,比如,我不能使用父类的引用变量调用Dog类特有的seeDoor()方法。

那如果我们就想使用子类特有的功能行不行呢?行!这就是我们下一章节所讲的内容:对象的转型

对象的转型

1.父类对象的引用指向子类对象,其实本质上是一个向上转型(向上,通过子类去实例化父类),就像int转成double一样,儿子穿了一身爸爸的衣服,扮成了爸爸(这里的儿子功能比爸爸的多)。

2.但变成了爸爸之后,只能使用爸爸特有的技能,儿子怎么能够使用自己本身的技能呢?这时候就需要向下转型,脱下伪装,将父类对象的引用强转成子类类型(要通过生成新的子类),就可以使用子类特有的技能了。

public class TestCasting {
    public static void main(String[] args) {
        Object obj = new String("北京尚学堂"); // 通过子类去实例化父类,向上可以自动转型
        // obj.charAt(0) 无法调用。编译器认为obj是Object类型而不是String类型
        /* 编写程序时,如果想调用运行时类型的方法,只能进行强制类型转换。
         * 不然通不过编译器的检查。 */
        String str = (String) obj; // 向下转型,向下转型,通过父类实例化子类,创建了一个子类新对象
        //其实这里的“父类”的真面目就是一个子类,相当于让这个“父类”卸去伪装。
        System.out.println(str.charAt(0)); // 位于0索引位置的字符
        System.out.println(obj == str); // true.他们俩运行时是同一个对象
    }
}
结果:
北
true

向下转型过程中,必须将引用变量转成真实的子类类型(运行时类型)否则会出现类型转换异常ClassCastException。

public class TestCasting2 {
    public static void main(String[] args) {
        Object obj = new String("北京尚学堂");
        //真实的子类类型是String,但是此处向下转型为StringBuffer
        StringBuffer str = (StringBuffer) obj;
        System.out.println(str.charAt(0));
    }
}

此时,栈中obj的值是堆中子类String的空间的地址。所以不能把String类型的对象变为StringBuffer类型。

为了避免出现这种异常,我们可以使用instanceof运算符进行判断

public class TestCasting3 {
    public static void main(String[] args) {
        Object obj = new String("北京尚学堂");
        //判断对象是哪个类的实例
        if(obj instanceof String){
            String str = (String)obj;
            System.out.println(str.charAt(0));
        }else if(obj instanceof StringBuffer){
            StringBuffer str = (StringBuffer) obj;
            System.out.println(str.charAt(0));
        }
    }
}
结果:
北

子类转化为父类(父类对象的引用指向子类对象):自动转换

上转型对象不能操作子类新增的成员变量和方法;

上转型对象可以操作子类继承或重写的成员变量和方法;

如果子类重写了父类的某个方法,上转型对象调用这个方法时,是调用重写的方法。

父类转化为子类(创建了一个子类新对象):强制转换

因为父类的对象 obj (Object obj = new String("北京尚学堂");) 的真面目就是一个子类,否则会出现类型转换错误。

final关键字

final关键字的作用:

  1. 修饰变量: 被他修饰的变量不可改变。一旦赋了初值,就不能被重新赋值
`final`  `int`   `MAX_SPEED = ``120``;`

  1. 修饰方法:该方法不可被子类重写。但是可以被重载(因为重载的方法只是名字相同而已)!
`final`  `void`  `study(){}`

子类中定义与继承方法同名不同参数列表的方法,这也叫重载

3.修饰类: 修饰的类不能被继承。比如:Math、String等。

`final`   `class`  `A {}`

抽象方法和抽象类

抽象方法

使用abstract修饰的方法,没有方法体,只有声明。定义的是一种“规范”,就是告诉子类必须要给抽象方法提供具体的实现。

如果子类也没有实现父类的抽象方法,那么子类也是一个抽象类。

抽象类

包含抽象方法的类就是抽象类。通过abstract方法定义规范,然后要求子类必须定义具体实现。通过抽象类,我们就可以做到严格限制子类的设计,使子类之间更加通用。

抽象类的意义在于给子类提供一个设计模板,让子类去实现功能。

抽象类和抽象方法的基本用法

//抽象类
abstract class Animal {
    abstract public void shout();  //抽象方法,只能放在抽象类里
}
class Dog extends Animal { 
    //子类必须实现父类的抽象方法,否则编译错误
    public void shout() {
        System.out.println("汪汪汪!");
    }
    //子类当然可以有父类不存在的方法
    public void seeDoor(){
        System.out.println("看门中....");
    }
}
//测试抽象类
public class TestAbstractClass {
    public static void main(String[] args) {
        Dog a = new Dog();// 也可以Animal a = new Dog();但是a.seeDoor();会用不了
        a.shout();
        a.seeDoor();
    }
}

抽象类的使用要点:

  1. 有抽象方法的类只能定义成抽象类
  2. 抽象类不能实例化,即不能用new来实例化抽象类。
Animal a = new Animal();  错误!
Animal a = new Dog();  可以!抽象类不能实例化,但是可以用来声明

  1. 抽象类可以包含属性方法构造方法。但是构造方法不能用来new实例,只能用来被子类调用。(什么意思?super?)
  2. 抽象类只能用来被继承
  3. 抽象方法必须被子类实现。(前面讲过可以不实现,但是子类也是抽象类)
  4. 父类不实现抽象方法,所以要注意:抽象方法没有大括号构成的方法体,有了大括号就成为了空实现。
abstract class Animal {
    String name;
    abstract public void shout();  //抽象方法,只能放在抽象类里
    abstract public void run(){}  //抽象方法没有大括号构成的方法体,错误!
}

接口

接口的作用

为什么需要接口?接口和抽象类的区别?

接口就是比“抽象类”还“抽象”的“抽象类”,可以更加规范的对子类进行约束。全面地专业地实现了:规范和具体实现的分离。

抽象类还提供某些具体实现,接口不提供任何实现,接口中所有方法都是抽象方法。接口是完全面向规范的,规定了一批类具有的公共方法规范。

1、抽象类和接口都不能直接实例化,如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。

2、抽象类要被子类继承,接口要被类实现。

3、接口只能做方法申明,抽象类中可以做方法申明,也可以做方法实现

4、接口里定义的变量只能是公共的静态的常量(默认是public static final 修饰),抽象类中的变量是普通变量。

5、抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,一个实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。

6、抽象方法只能申明,不能实现,接口是设计的结果 ,抽象类是重构的结果

7、抽象类里可以没有抽象方法

8、如果一个类里有抽象方法,那么这个类只能是抽象类

9、抽象方法要被实现,所以不能是静态的,也不能是私有的。

10、接口可继承接口,并可多继承接口,但类只能单根继承。

从接口的实现者角度看,接口定义了可以向外部提供的服务。

从接口的调用者角度看,接口定义了实现者能提供那些服务。

接口是两个模块之间通信的标准,通信的规范。如果能把你要设计的模块之间的接口定义好,就相当于完成了系统的设计大纲,剩下的就是添砖加瓦的具体实现了。大家在工作以后,做系统时往往就是使用“面向接口”的思想来设计系统。

接口和实现类不是父子关系,是实现规则的关系。比如:我定义一个接口Runnable,Car实现它就能在地上跑,Train实现它也能在地上跑,飞机实现它也能在地上跑。就是说,如果它是交通工具,就一定能跑,但是一定要实现Runnable接口。

接口的本质探讨

接口就是规范,定义的是一组规则,体现了现实世界中“如果你是…则必须能…”的思想。如果你是天使,则必须能飞。如果你是汽车,则必须能跑。如果你是好人,则必须能干掉坏人;如果你是坏人,则必须欺负好人。

接口的本质是契约,就像我们人间的法律一样。制定好后大家都遵守。

面向对象的精髓,是对对象的抽象,最能体现这一点的就是接口。为什么我们讨论设计模式都只针对具备了抽象能力的语言(比如C++、Java、C#等),就是因为设计模式所研究的,实际上就是如何合理的去抽象。

区别

  1. 普通类:具体实现
  2. 抽象类:具体实现,规范(抽象方法)
  3. 接口:规范!

接口里面的方法不用加abstract修饰,因为接口里面的方法一定是抽象方法

因为接口里面只有抽象方法常量,具体怎样去实现还是要继承了接口的类自己去决定,所以比较稳定。

如何定义和使用接口?

声明格式:

[访问修饰符]  interface 接口名   [extends  父接口1,父接口2]  {
常量定义;  
方法定义;
}

定义接口的详细说明:

  1. 访问修饰符:只能是public或默认。
  2. 接口名:和类名采用相同命名机制。
  3. extends:接口可以多继承。
  4. 常量:接口中的属性只能是常量,总是:public static final 修饰。不写也默认是。
  5. 方法:接口中的方法只能是:public abstract。 省略的话,也是public abstract。

要点

  1. 子类通过implements来实现接口中的规范。
  2. 接口不能创建实例,但是可用于声明引用变量类型
对于Volant接口:
类:class Angel implements Volant, Honest
有: Volant volant = new Angel(); //正确
	new Volant(); //错误

  1. 一个非抽象类实现了接口,必须实现接口中所有的方法,并且这些方法只能是public的。
  2. JDK1.7之前,接口中只能包含静态常量抽象方法,不能有普通属性、构造方法、普通方法。
  3. JDK1.8后,接口中包含普通的静态方法

接口的使用

public class TestInterface {
    public static void main(String[] args) {
        Volant volant = new Angel();
        volant.fly();
        System.out.println(Volant.FLY_HIGHT);
         
        Honest honest = new GoodMan();
        honest.helpOther();
    }
}
/**飞行接口*/
interface Volant { 
    int FLY_HIGHT = 100;  // 总是:public static final类型的;
    void fly();   //总是:public abstract 修饰的;
}
/**善良接口*/
interface Honest { 
    void helpOther();
}
/**Angle类实现飞行接口和善良接口*/
class Angel implements Volant, Honest{
    public void fly() {
        System.out.println("我是天使,飞起来啦!");
    }
    public void helpOther() {
        System.out.println("扶老奶奶过马路!");
    }
}
class GoodMan implements Honest {
   public void helpOther() {
        System.out.println("扶老奶奶过马路!");
    }  
}
class BirdMan implements Volant {
    public void fly() {
        System.out.println("我是鸟人,正在飞!");
    }
}
结果:
	我是天使,飞起来啦!
	100
	扶老奶奶过马路!

Volant volant =new Angel();中用接口来声明引用变量类型(类,接口,数组),即说明该引用变量是接口,编译器会把创建的这个angel()当成是Volant类型的接口,只能使用Volant接口的常量和实现Volant定义的方法。

假设在Angel类新增了一个方法 eat()

/**Angle类实现飞行接口和善良接口,以及增加了吃饭方法*/
class Angel implements Volant, Honest{
    public void fly() {
        System.out.println("我是天使,飞起来啦!");
    }
    public void helpOther() {
        System.out.println("扶老奶奶过马路!");
    }
    public void eat(){
        System.out.println("吃东西");
    }
}

	Volant volant = new Angel();
    volant.eat();   //报错!
//java的实现类可以添加接口外的方法,但是声明对象时只有声明本身类才能调用到。
    Angel angel = new Angel();
    angel.eat();    //成功!

但是不可以用Volant volant =new Volant();因为接口不能用来创建实例。

可以使用接口声明对象,但必须使用其实现类实例化,接口实例化报错。

接口的多继承

接口完全支持多继承。和类的继承类似,子接口扩展某个父接口,将会获得父接口中所定义的一切。

类可以实现多个接口;接口也可以继承多个接口,实现该接口的类需要实现接口的所有方法。

类没有多继承,接口才有多继承。

接口的多继承

interface A {
    void testa();
}
interface B {
    void testb();
}
/**接口可以多继承:接口C继承接口A和B*/
interface C extends A, B {
    void testc();
}
public class Test implements C {
    public void testc() {
    }
    public void testa() {
    }
    public void testb() {
    }
}

面向接口编程

面向接口编程是面向对象编程的一部分。

​ 为什么需要面向接口编程? 软件设计中最难处理的就是需求的复杂变化,需求的变化更多的体现在具体实现上。我们的编程如果围绕具体实现来展开就会陷入”复杂变化”的汪洋大海中,软件也就不能最终实现。我们必须围绕某种稳定的东西开展,才能以静制动,实现规范的高质量的项目。

接口就是规范,就是项目中最稳定的东东! 面向接口编程可以让我们把握住真正核心的东西,使实现复杂多变的需求成为可能。

​ 通过面向接口编程,而不是面向实现类编程,可以大大降低程序模块间的耦合性,提高整个系统的可扩展性和和可维护性。

​ 面向接口编程的概念比接口本身的概念要大得多。设计阶段相对比较困难,在你没有写实现时就要想好接口,接口一变就乱套了,所以设计要比实现难!

老鸟建议

​ 接口语法本身非常简单,但是如何真正使用?这才是大学问。我们需要后面在项目中反复使用,大家才能体会到。 学到此处,能了解基本概念,熟悉基本语法,就是“好学生”了。 请继续努力!再请工作后,闲余时间再看看上面这段话,相信你会有更深的体会。

内部类

内部类的概念

一般情况,我们把类定义成独立的单元。有些情况下,我们把一个类放在另一个类的内部定义,称为内部类(innerclasses)。

内部类可以使用public、default、protected 、private以及static修饰。而外部顶级类(我们以前接触的类)只能使用public和default修饰。

注意

​ 内部类只是一个编译时概念,一旦我们编译成功,就会成为完全不同的两个类。对于一个名为Outer的外部类和其内部定义的名为Inner的内部类。编译完成后会出现Outer.class和Outer$Inner.class两个类的字节码文件。所以内部类是相对独立的一种存在,其成员变量/方法名可以和外部类的相同。
在这里插入图片描述

/**外部类Outer*/
class Outer {
    private int age = 10;
    public void show(){
        System.out.println(age);//10
    }
    /**内部类Inner*/
    public class Inner {
        //内部类中可以声明与外部类同名的属性与方法,但是内部类会将其隐藏
        private int age = 20;
        public void show(){
            System.out.println(age);//20
        }
    }
}

内部类的作用:

  1. 内部类提供了更好的封装。只能让外部类直接访问,不允许同一个包中的其他类直接访问。

  2. 内部类可以直接访问外部类的私有属性,内部类被当成其外部类的成员。 但外部类不能访问内部类的内部属性

    补充:成员内部类虽然在本类内部,但是封装级别比本类更高,
    
    所以想要正常访问内部类,需要创建内部类对象,通过对象名来访问,
    
    而内部类本身就处在外部类内部,所以可以直接访问外部类
    
    
  3. 接口只是解决了多重继承的部分问题,而内部类使得多重继承的解决方案变得更加完整。

内部类的使用场合:

  1. 由于内部类提供了更好的封装特性,并且可以很方便的访问外部类的属性。所以,在只为外部类提供服务的情况下可以优先考虑使用内部类。
  2. 使用内部类间接实现多继承:每个内部类都能独立地继承一个类或者实现某些接口,所以无论外部类是否已经继承了某个类或者实现了某些接口,对于内部类没有任何影响。

内部类的分类

在Java中内部类主要分为成员内部类(非静态内部类、静态内部类)、匿名内部类、局部内部类。

成员内部类:非静态内部类,静态内部类

可以使用private、default、protected、public任意进行修饰。 类文件:外部类$内部类.class

a) 非静态内部类(外部类里使用非静态内部类和平时使用其他类没什么不同)

i. 非静态内部类必须寄存在一个外部类对象里。因此,如果有一个非静态内部类对象那么一定存在对应的外部类对象。非静态内部类对象单独属于外部类的某个对象

ii. 非静态内部类可以直接访问外部类的成员,但是外部类不能直接访问非静态内部类成员。

iii. 非静态内部类不能有静态方法、静态属性和静态初始化块

iv. 外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例。

v. 成员变量访问要点:

  1. 内部类里方法的局部变量:变量名。
  2. 内部类属性:this.变量名。
  3. 外部类属性:外部类名.this.变量名。
class Outer {
    private int age = 10;
    class Inner {
        int age = 20;
        public void show() {
            int age = 30;
            System.out.println("内部类方法里的局部变量age:" + age);// 30
            System.out.println("内部类的成员变量age:" + this.age);// 20
            //外部类.this.成员变量
            System.out.println("外部类的成员变量age:" + Outer.this.age);// 10
        }
    }
}

vi. 内部类的访问:

  1. 外部类中定义内部类:
new Inner()

  1. 外部类以外的地方使用非静态内部类:
Outer.Inner  varname = new Outer().new Inner()

内部类的访问实例代码

public class TestInnerClass {
    public static void main(String[] args) {
        //先创建外部类实例,然后使用该外部类实例创建内部类实例
        Outer.Inner inner = new Outer().new Inner();
        inner.show();
        Outer outer = new Outer();
        Outer.Inner inn = outer.new Inner();
        inn.show();
    }
}

b) 静态内部类

i. 定义方式:

static  class   ClassName {
//类体
}

ii. 使用要点:

  1. 当一个静态内部类对象存在,并不一定存在对应的外部类对象。 因此,静态内部类的实例方法不能使用外部类的非static成员变量或者方法。
    2. 静态内部类看做外部类的一个静态成员。 因此,外部类的方法中可以通过:

静态内部类.名字”的方式访问静态内部类的静态成员,

通过 **new 静态内部类()**访问静态内部类的实例。

如果是其他类要访问内部类,则需要在上面的方法前面再加上外部类

class Outer{
    //访问静态内部类的静态成员
    int b = Inner.a;
    //在外部类访问内部类的实例
    Inner inner = new Inner();
    //相当于外部类的一个静态成员
    static class Inner{
        static int a = 1;
    }
}
public class TestStaticInnerClass {
    public static void main(String[] args) {
         //直接使用:外部类.静态内部类.静态成员 的方法访问 静态内部类的静态成员
         System.out.println(Outer.Inner.b);
        
        //通过 new 外部类名.内部类名() 来创建内部类对象
        Outer.Inner inner =new Outer.Inner();       
    }
}

匿名内部类

适合那种只需要使用一次的类。比如:键盘监听操作等等。

new  父类构造器(实参类表) \实现接口 () {
           //匿名内部类类体!
}

interface SpeakHello{
    void speak();
}
class HelloMachine{
    public void turnOn(SpeakHello hello) { //类里面的方法需要用到接口
        System.out.println("*********************");//main使用的方法会传到这里,并且实现
        hello.speak();					   //使用接口的方法
    }
}

//解析匿名类的意义
public class Test {
    public static void main(String[] args) {
        HelloMachine machine=new HelloMachine();
        //新方法
        //用接口名和一个类体创建一个匿名对象,然后将其传入turnOn()方法
        machine.turnOn(new SpeakHello() {
            //此时类体要重写接口中的全部方法
            @Override
            public void speak() {
                System.out.println("hello,you are welcome!");
            }
        });
        //旧方法
        //用接口的实现类创建对象 shc,传入turnOn()做参数
        SpeakHelloClass shc = new SpeakHelloClass();
        machine.turnOn(shc);
    }
}
//旧方法:接口的实现类,用于创建对象
class SpeakHelloClass implements SpeakHello{
    @Override
    public void speak() {
        System.out.println("你好,这是要创建接口实现类的老方法");
    }
}
结果:
*********************
hello,you are welcome!
*********************
你好,这是要创建接口实现类的老方法

注意:

  1. 匿名内部类没有访问修饰符。
  2. 匿名内部类没有构造方法。因为它连名字都没有那又何来构造方法呢。
    在这里插入图片描述

局部内部类

还有一种内部类,它是定义在方法内部的,作用域只限于本方法,称为局部内部类。

局部内部类的的使用主要是用来解决比较复杂的问题,想创建一个类来辅助我们的解决方案,到那时又不希望这个类是公共可用的,所以就产生了局部内部类。局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法中被使用,出了该方法就会失效。

局部内部类在实际开发中应用很少。

String类

String基础

  1. String类又称作不可变字符序列。
源码如下:
/** The value is used for character storage. */
private final char value[];
//String是一个char类型的数组,又因为被final修饰,所以不可变,只能被初始化一次。
  1. String位于java.lang包中,Java程序默认导入java.lang包下的所有类
  2. Java字符串就是Unicode字符序列,例如字符串“Java”就是4个Unicode字符’J’、’a’、’v’、’a’组成的。
  3. Java没有内置的字符串类型,而是在标准Java类库中提供了一个预定义的类String每个用双引号括起来的字符串都是String类的一个实例
//String类型的初始化:
public class TestString {
    public static void main(String[] args) {
        String e = ""  ; // 空字符串
        String str = "abc";
        String str2 = new String("def");
        String str3 = str + str2;
        String str4 =  "18" + 19;
        System.out.println(str4);
    }
}
  1. Java允许使用符号"+"把两个字符串连接起来。
String s1 = "Hello";
String s2 = "World! ";
String s = s1 + s2; //HelloWorld!

符号"+"把两个字符串按给定的顺序连接在一起,并且是完全按照给定的形式。

当"+"运算符两侧的操作数中只要有一个是字符串(String)类型,系统会自动将另一个操作数转换为字符串然后再进行连接。

int age = 18;
String str = "age is " + age;  //str赋值为"age is 18"
//这种特性通常被用在输出语句中:
System.out.println("age  is" + age);

String类和常量池

在Java的内存分析中,我们会经常听到关于“常量池”的描述,实际上常量池也分了以下三种:

1. 全局字符串常量池(String Pool)

​ 全局字符串常量池中存放的内容是在类加载完成后存到String Pool中的,在每个VM中只有一份,存放的是字符串常量的引用值(在堆中生成字符串对象实例)。

2. class文件常量池(Class Constant Pool)

​ class常量池是在编译的时候每个class都有的,在编译阶段,存放的是常量(文本字符串、final常量等)和符号引用。

3. 运行时常量池(Runtime Constant Pool)

​ 运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。

String str1 = "abc";
String str2 = new String("def");
String str3 = "abc";
String str4 = str2.intern();
String str5 = "def";
System.out.println(str1 == str3);// true
System.out.println(str2 == str4);// false
System.out.println(str4 == str5);// true

首先经过编译之后,在该类的class常量池中存放一些符号引用,然后类加载之后,将class常量池中存放的符号引用转存到运行时常量池中,然后经过验证,准备阶段之后,在堆中生成驻留字符串的实例对象(也就是上例中str1所指向的“abc”实例对象),然后将这个对象的引用存到全局String Pool中,也就是String Pool中,最后在解析阶段,要把运行时常量池中的符号引用替换成直接引用,那么就直接查询String Pool,保证String Pool里的引用值与运行时常量池中的引用值一致,大概整个过程就是这样了。

​ 回到示例5-28的那个程序,现在就很容易解释整个程序的内存分配过程了,首先,在堆中会有一个“abc”实例,全局String Pool中存放着“abc”的一个引用值,然后在运行第二句的时候会生成两个实例,一个是“def”的实例对象,并且String Pool中存储一个“def”的引用值,还有一个是new出来的一个“def”的实例对象,与上面那个是不同的实例,当在解析str3的时候查找String Pool,里面有“abc”的全局驻留字符串引用,所以str3的引用地址与之前的那个已存在的相同,str4是在运行的时候调用intern()函数,返回String Pool中“def”的引用值,如果没有就将str2的引用值添加进去,在这里,String Pool中已经有了“def”的引用值了,所以返回上面在new str2的时候添加到String Pool中的 “def”引用值,最后str5在解析的时候就也是指向存在于String Pool中的“def”的引用值,那么这样一分析之后,结果就容易理解了。

字符串以及被拼接产生的字符串会被放到常量池里面,相同的字符串所在的存储地址是一样的;

通过new方法产生的字符串的值是在对象里面,地址与直接被赋值的字符串不同。

public static void main(String[] args) {
        String str = "a";
        String str2 = "d";
        String str3 = "a"+"d";  //常量池中
        String str4 = str + str2;	//对象
        String str5 =  "ad";	//常量池中


        System.out.println(str3==str4);		false
        System.out.println(str5==str3);		true
        System.out.println(str3=="ad");		true
        System.out.println(str4=="ad");		false
    }

String类常用的方法

String类是我们最常使用的类。字符串类的方法我们必须非常熟悉!我们列出常用的方法,请大家熟悉。

要多注意返回值的类型是boolean 还是int或其他,有助于我们更好的理解这些方法。
在这里插入图片描述

String类常用方法一

public class StringTest1 {
    public static void main(String[] args) {
        String s1 = "core Java";
        String s2 = "Core Java";
        System.out.println(s1.charAt(3));//提取下标为3的字符	e
        System.out.println(s2.length());//字符串的长度(包括空格)	9
        System.out.println(s1.equals(s2));//比较两个字符串是否相等	false
        System.out.println(s1.equalsIgnoreCase(s2));//比较两个字符串(忽略大小写)	true
        System.out.println(s1.indexOf("Java"));//字符串s1中是否包含Java		5
        System.out.println(s1.indexOf("apple"));//字符串s1中是否包含apple	-1
        String s = s1.replace(' ', '&');//将s1中的空格替换成&	core&Java
        System.out.println("result is :" + s); 
    }
}

String类常用方法二

public class StringTest2 {
    public static void main(String[] args) {
        String s = "";
        String s1 = "How are you?";
        System.out.println(s1.startsWith("How"));//是否以How开头	true
        System.out.println(s1.endsWith("you"));//是否以you结尾	false
        s = s1.substring(4);//提取子字符串:从下标为4的开始到字符串结尾为止	are you?
        System.out.println(s);
        s = s1.substring(4, 7);//提取子字符串:下标[4, 7) 不包括7	are
        System.out.println(s);
        s = s1.toLowerCase();//转小写	how are you
        System.out.println(s);
        s = s1.toUpperCase();//转大写	HOW ARE YOU
        System.out.println(s);
        String s2 = "  How old are you!! ";
        s = s2.trim();//去除字符串首尾的空格。注意:中间的空格不能去除		How old are you!!
        System.out.println(s);	
        System.out.println(s2);//因为String是不可变字符串,所以s2不变
    }
}

字符串相等的判断

  1. equals方法用来检测两个字符串内容是否相等。如果字符串s和t内容相等,则s.equals(t)返回true,否则返回false。
  2. 要测试两个字符串除了大小写区别外是否是相等的,需要使用equalsIgnoreCase方法。
  3. 判断字符串是否相等不要使用"=="
  4. String的equal是重写过的,比较的是字符串的内容。

忽略大小写的字符串比较

"Hello".equalsIgnoreCase("hellO");//true

字符串的比较"=="与equals()方法

public class TestStringEquals {
    public static void main(String[] args) {
        String g1 = "北京尚学堂";
        String g2 = "北京尚学堂";
        String g3 = new String("北京尚学堂");
        System.out.println(g1 == g2); // true  指向同样的字符串常量对象
        System.out.println(g1 == g3); // false  g3是新创建的对象
        System.out.println(g1.equals(g3)); // true  g1和g3里面的字符串内容是一样的
    }
}

内存分析如图:
在这里插入图片描述

开闭原则

开闭原则(Open-Closed Principle)就是让设计的系统对扩展开放,对修改封闭。

· 对扩展开放:

​ 就是指,应对需求变化要灵活。 要增加新功能时,不需要修改已有的代码,增加新代码即可。

· 对修改关闭:

​ 就是指,核心部分经过精心设计后,不再因为需求变化而改变。

​ 在实际开发中,我们无法完全做到,但应尽量遵守开闭原则。

模板方法模式和回调机制

模板方法模式很常用,其目的是在一个方法中定义一个算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法的某些步骤。在标准的模板方法模式实现中,主要是使用继承的方式,来让父类在运行期间可以调用到子类的方法。 详见抽象类部分示例。

​ 其实在Java开发中,还有另外一个方法可以实现同样的功能,那就是Java回调技术。回调是一种双向的调用模式,也就是说,被调用的接口被调用时也会调用对方的接口,简单点说明就是:A类中调用B类中的C方法,然后B类中的C方法中反过来调用A类中的D方法,那么D这个方法就叫回调方法。

  回调的具体过程如下:
​1. Class A实现接口CallBack —— 背景1
​2. class A中包含class B的引用 ——背景2
​3. class B有一个参数为CallBack的方法C ——背景3
​4. 前三条是我们的准备条件,接下来A的对象调用B的方法C
​5. 然后class B就可以在C方法中调用A的方法D

​ 这样说大家可能还是不太理解,下面我们根据示例5-33来说明回调机制。该示例的生活背景为:有一天小刘遇到一个很难的问题“学习Java选哪家机构呢?”,于是就打电话问小高,小高一时也不太了解行情,就跟小刘说,我现在还有事,等忙完了给你咨询咨询,小刘也不会傻傻的拿着电话去等小高的答案,于是小刘对小高说,先挂电话吧,你知道答案后再打我电话告诉我吧,于是挂了电话。小高先去办自己的事情去了,过了几个小时,小高打电话给小刘,告诉他答案是“学Java当然去北京尚学堂”。
在这里插入图片描述

/** 
 * 回调接口  
 */
interface CallBack {  
    /** 
     * 小高知道答案后告诉小刘时需要调用的方法,即回调方法
     * @param result 是问题的答案 
     */  
    public void answer(String result);  
}
/** 
 * 小刘类:实现了回调接口CallBack(背景一) 
 */  
class Liu implements CallBack {  
    /** 
     * 包含小高对象的引用 (背景二) 
     */  
    private Gao gao;   
  
    public Liu(Gao gao){  
        this.gao = gao;  
    }  
       
    /** 
     * 小刘通过这个方法去问小高 
     * @param question  小刘问的问题“学习Java选哪家机构呢?” 
     */  
    public void askQuestion(String question){  
        //小刘问小高问题
        //这个Liu.this的含义:调用Liu这个类实例化后的对象
        //这里多此一举,可以只用this就行
        gao.execute(Liu.this, question);          
    }  
    /** 
     * 小高知道答案后调用此方法告诉小刘
     */  
    @Override
    public void answer(String result) {
        System.out.println("小高告诉小刘的答案是:" + result);        
    }  
} 
/** 
 * 小高类 
 */  
class Gao {
     /** 
     * 相当于class B有一个参数为CallBack的方法C(背景三) 
     */  
    public void execute(CallBack callBack, String question){  
        System.out.println("小刘问的问题是:" + question);  
        //模拟小高挂点后先办自己的事情花了很长时间  
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //小高办完自己的事情后想到了答案 
        String result = "学Java当然去北京尚学堂";  
        //小高打电话把答案告诉小刘,相当于class B 反过来调用class A 的D方法 
        callBack.answer(result);         
    }  
}
 
public class Test {
    public static void main(String[] args) {
        Gao  gao= new Gao();  
        Liu liu = new Liu(gao);  
        //小刘问问题
        liu.askQuestion("学习Java选哪家机构呢?"); 
    } 
}
结果:
小刘问的问题是:学习Java选哪家机构呢?
小高告诉小刘的答案是:学Java当然去北京尚学堂

通过回调在接口中定义的方法,调用到具体的实现类中的方法,其本质是利用Java的动态绑定技术,在这种实现中,可以不把实现类写成单独的类,而使用内部类或匿名内部类来实现回调方法。

组合模式

组合模式是将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

class Cpu {
    public void run() {
        System.out.println("quickly.........");
    }
}
class MainBoard {
    public void connect() {
        System.out.println("connect...........");
    }
}
class Memory {
    public void store() {
        System.out.println("store........");
    }
}
public class Computer {
    Cpu cpu;
    Memory memory;
    MainBoard mainBoard;
 
    public void work() {
        cpu.run();
        memory.store();
        mainBoard.connect();
    }
     
    public static void main(String[] args) {
        Computer computer = new Computer();
        computer.cpu = new Cpu();
        computer.mainBoard = new MainBoard();
        computer.memory = new Memory();
        computer.work();
    }
}

Java类初始化顺序说明

一个类中包含如下几类东西,他们前后是有顺序关系的

  1. 静态属性:static 开头定义的属性
  2. 静态方法块: static {} 圈起来的方法块
  3. 普通属性: 未带static定义的属性
  4. 普通方法块: {} 圈起来的方法块
  5. 构造函数: 类名相同的方法
  6. 方法: 普通方法

接口静态变量

父类静态变量/父类静态代码块
(同级,按代码顺序执行)
子类静态变量/子类静态代码块
(同级,按代码顺序执行)

父类普通变量/父类普通代码块
(同级,按代码顺序执行)
父类构造函数

子类普通变量/子类普通代码块
(同级,按代码顺序执行)
子类构造函数

注意点

静态内容只在类加载时执行一次,之后不再执行。

默认调用父类的无参构造方法,可以在子类构造方法中利用super指定调用父类的哪个构造方法。

public class LifeCycle {
    // 静态属性
    private static String staticField = getStaticField();
    // 静态方法块
    static {
        System.out.println(staticField);
        System.out.println("静态方法块初始化");
    }
    // 普通属性
    private String field = getField();
    // 普通方法块
    {
        System.out.println(field);
        System.out.println("普通方法块初始化");
    }
    // 构造函数
    public LifeCycle() {
        System.out.println("构造函数初始化");
    }

    public static String getStaticField() {
        String statiFiled = "Static Field Initial";
        System.out.println("静态属性初始化");
        return statiFiled;
    }

    public static String getField() {
        String filed = "Field Initial";
        System.out.println("普通属性初始化");
        return filed;
    }   
    // 主函数
    public static void main(String[] argc) {
        new LifeCycle();
    }
}
结果:
静态属性初始化
Static Field Initial
静态方法块初始化
普通属性初始化
Field Initial
普通方法块初始化
构造函数初始化
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值