《疯狂Java讲义(第4版)》-----第5章(Java核心)【面向对象(上)】

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ccnuacmhdu/article/details/82888301

类用于描述客观世界里某一类对象的共同特征,而对象则是类的具体存在。比如人类和具体的人。

类的定义及对象的创建

一个类包括成员变量、方法、构造器
在这里插入图片描述
说明:

  • static修饰的成员变量、方法是类级别的,属于该类本身及其所有实例,可以由类及其实例对象调用。没有static修饰的,则是对象(实例)级别的,没有实例则无法使用。
  • 类的各成员定义顺序随意,各成员之间可以互相调用,但注意static修饰的只能调用static修饰的。建议调用类级别的static成员变量、方法用类去调用,不要用对象去调用
  • pulibc Hello(){}是构造器。如果写成public void Hello(){},就成了一个非构造器的方法了。构造器定义的时候不能写返回值类型,其实构造器是有返回值的,返回的就是当前的这个实例对象,我们用new关键字创建对象的时候,调用构造器,创建了一个该类的对象。
  • 成员变量可以指定默认初始值,如果不指定的话,系统会默认初始化如下:
    在这里插入图片描述
    进入内存看对象Hello h = new Hello();

在这里插入图片描述
引用变量本质是是指针,是地址。上图说明,h仅仅是一个指向堆内存里面对象的一个引用(本质的指针的封装)。这就很好体现了Java面向对象语言的封装机制,不允许直接访问堆内存里面的对象,而是通过一个引用变量来访问

this关键字

Java中的方法不能独立存在,必须依赖类或对象,调用方式是类.方法,对象.方法,如果省略了调用者,系统会自动加上,如果是静态方法,系统自动加上类名,如果是非静态方法,系统自动加上this

代码1

this指向当前对象。下面代码中,fun2调用fun1。this.fun1();可以省去this写成fun1();其实程序员省略了,系统还是会帮忙加上!



public class Person{

	public void fun1(){
		System.out.println("hello");
	}
	
	public void fun2(){
		this.fun1();
	}

}
代码2

下面代码编译会出现下面错误:
Person.java:10: 错误: 无法从静态上下文中引用非静态 方法 fun1()
fun1();
^
1 个错误

main函数中调用fun1(),在同一个类内部调用,省略了this,系统会加上,实质是this.fun1(),但是main方法是静态的,是类级别的,不能调用对象级别的函数fun1。

public class Person{

	public void fun1(){
		System.out.println("hello");
	}

	public static void main(String[] args){
		fun1();
	}
}

解决办法是
方案1:把fun1定义为静态,然后调用方式是fun1()(省略了Person);或Person.fun1()
方案2:;或者新建对象后,用对象去调用fun1,可以是一句代码new Person().fun1();

代码3

下面代码很奇葩:

class ReturnThis{

	public int n;
	public ReturnThis grow(){
		n++;
		return this;
	}
	public static void main(String[] args){
		ReturnThis rt = new ReturnThis();
		rt.grow().grow().grow();
		System.out.println("n="+rt.n);
	}

}

打印结果是:n=3

方法的参数传递机制

Java中方法参数传递只有值传递方式,形参是实参的复制品。
代码1
下面代码不能实现数字交换

public class Hello{

	public static void swap(int a, int b){
		int tmp = a;
		a = b;
		b = tmp;
	}
	public static void main(String[] args){
		int a = 1;
		int b = 2;
		Hello.swap(a,b);
		System.out.println("a="+a+",b="+b);
	}

}

在这里插入图片描述
代码2

class Data{
	int a;
	int b;
}

public class Hello{

	public static void swap(Data d){
		int tmp = d.a;
		d.a = d.b;
		d.b = tmp;
	}
	public static void main(String[] args){
		Data d = new Data();
		d.a = 1;
		d.b = 2;
		Hello.swap(d);
		System.out.println("a="+d.a+",b="+d.b);
	}

}

此代码可以实现a,b的交换。原因是对象是引用变量,复制一份给形参,形参获得了这个对象的地址,操作形参,就是在操作实际对象。
在这里插入图片描述

形参个数可变的方法

public class Hello{

public static void test(int n, String...cities){
	for(String city:cities){
		System.out.println(city);
	}
}
public static void main(String[] args){
	Hello.test(3,"北京","上海","广州");
}

}
输出:
北京
上海
广州

代码可以改为:


public class Hello{

	public static void test(int n, String...cities){
		for(String city:cities){
			System.out.println(city);
		}
	}
	public static void main(String[] args){
		Hello.test(3,new String[]{"北京","上海","广州"});
	}

}

说明:

  • 个数可变的形参最多只能有一个,并且位于形参列表的末尾。
  • 可变形参本质是个数组,所以对于可变形参来说传入数组或多个参数都可以

递归方法


public class Hello{

	public static int f(int n){
		if(n == 1 | n == 2){
			return 1;
		}else{
			return f(n-1) + f(n-2);
		}
	}
	public static void main(String[] args){
		System.out.println(Hello.f(5));
	}

}

方法重载

方法重载:同一类中的方法名相同且参数列表不同。返回值类型,修饰符与方法重载无关。之所以不能用返回值类型来重载方法,原因是比如如下两个函数f,如果这么调用:f();则无法判断执行的哪个f:

public void f(){}
public int f(){}

含有可变形参的方法重载


public class Hello{

	public void test(String s){
		System.out.println("public void test(String s)");
	}
	public void test(String...s){
		System.out.println("public void test(String...s)");

	}
	
	public static void main(String[] args){
		Hello h = new Hello();
		h.test(new String[]{"fds"});//执行public void test(String...s)
		h.test("fds");//执行public void test(String s)
		h.test("fdas", "fda");//执行public void test(String...s)
	}

}

成员变量和局部变量

Java中的变量分为成员变量和局部变量。

  • 成员变量:类变量和实例变量。在堆内存。
  • 局部变量:方法的形参,方法的局部变量,代码块中的局部变量。在栈内存。
    初始化
    成员变量可以在类定义的时候就指定初始默认值,如果不指定,系统在类的准备阶段或创建该类的实例的同时进行默认初始化,初始化规则是:
    在这里插入图片描述
    局部变量必须显式初始化,系统不会为局部变量进行默认初始化。如果不显式初始化,局部变量不会分配内存,不能进行访问,如果访问未初始化的局部变量,编译报错。
    销毁
    局部变量在栈内存中,无须系统垃圾回收,随着方法或代码块的运行结束就自动销毁了。局部变量的值只保存基本类型的值或对象的引用,占据内存通常小。尽量缩小局部变量的作用范围,可以减少局部变量在内存中的生存时间,提高程序性能。

封装

把对象的成员变量和实现细节隐藏起来,暴露出一些方法。让方法控制对成员的访问和安全操作。

访问控制级别表

private default protected public
同一个类 可以 可以 可以 可以
同一个包 可以 可以 可以
子类中 可以 可以
全局范围 可以

模块设计追求高内聚(尽可能把模块内部数据、功能实现细节隐藏在内部,不允许外界直接干预)、低耦合(尽量暴露出少量的方法供外界调用)。

package

放在包下的类的编译运行

在此补充命令行执行Java源码的方法:javac -d 目标目录 文件名,对.java文件编译后放在目标目录下,特别是放在包下的类,要这么编译,否则,可以编译成功,但无法生成包对应的文件夹,也无法执行类。例子如下:

package p;

public class Hello{

	public static void main(String[] args){
		
		System.out.println("hello");
		
	}

}

在这里插入图片描述
在当前目录下生成包p对应的文件夹及里面的字节码文件:
在这里插入图片描述
在这里插入图片描述

  • 多重包,包中有包的情况下,比如上面的代码中更改第一句为package p.a,再次编译运行及当前目录下生成的文件夹、字节码情况如下:

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

  • 使用别的包下的类,可以创建这个类的时候带上完整包名(包括父包),也可以用import语句引进来。

  • Java默认导入了java.lang包下的所有类。

  • 如果两个包下含有同名的类,同时都引入了进来,那么使用这样的类必须用全包名。比如

    import java.util.*;
    import java.sql.*;
    
    java.uti.Date d = new java.util.Date();//必须要带全包名 ```
    
  • import static java.lang.Math.*;的import static语句引入静态方法或变量

构造器

Java对象的创建是构造器完全负责的吗?
不是。程序员调用构造器时,系统先为对象分配了内存空间,并为对象执行了默认初始化(这个对象已经生成,在构造器中可以用this引用),之后才是构造器执行体的执行。构造器执行体执行结束,返回该对象的引用。

一个构造器中调用另一个构造器的实例:

**注意:一个构造器中用this调用其他构造器,this必须是该构造器中的第一个语句。**如下代码示:


public class Hello{

	private String name;
	private String id;
	
	public Hello(){}

	public Hello(String id){
		this.id = id;
		System.out.println("public Hello(String id)");
	}
	public Hello(String id, String name){
		
		this(id);//对this的调用必须是构造器中的第一个语句
		this.name = name;
		System.out.println("public Hello(String id, String name)");
	}
	
	public static void main(String[] args){
		
		new Hello("123", "Tom");
		
	}

}

类的继承

Java的继承特点:单继承(一个类最多只能有一个直接父类)。

  • 子类继承父类,可以获得父类的所有成员变量和方法(非private),但不能获得父类的构造器。
  • 如果一个Java类未显式继承一个父类,那么这个类默认继承java.lang.Object类

重写父类的方法

方法重写遵循“两同两小一大”规则:

  • 相同的方法名和形参列表
  • 子类方法的返回值类型比父类的小或相等
  • 子类方法声明抛出的异常类比父类方法声明抛出的异常类小或相等
  • 子类方法的访问权限比父类方法的访问权限大或相等

还要注意,static修饰的和非static修饰的方法之间不可相互覆盖。由于子类获取不到父类的private方法,如果子类按照重写规则写一个父类的方法,这个方法并不是重写,而是子类自己定义的方法,和父类的方法无关。

下面例子是鸵鸟类继承鸟类,而鸵鸟不会飞,所以要重写fly方法。

class Bird{

	public void fly(){
		System.out.println("I can fly...");
	}
}

class Ostrich extends Bird{

	public void fly(){
		System.out.println("I can only run on the land.");
	}

	public static void main(String[] args){
		new Ostrich().fly();
	}
}

如何调用被覆盖的父类方法或变量?
使用super关键字。super关键字指的就是父类的实例。注意是实例对象,如果调用父类被覆盖的静态变量,应该用父类类名调用。

class Bird{

	public void fly(){
		System.out.println("I can fly...");
	}
}

class Ostrich extends Bird{

	public void fly(){
		System.out.println("I can only run on the land.");
		super.fly();//调用父类被覆盖的方法
	}

	public static void main(String[] args){
		new Ostrich().fly();
	}
}

打印输出:
I can only run on the land.
I can fly…

覆盖内涵??
子类覆盖父类的方法或变量,并不是替代!实际上,子类对象创建时,除了创建子类自身的变量和方法外,还会创建他所继承的所有父类(直接父类和间接父类)的方法和变量,他覆盖掉的父类的方法和变量只是被隐藏了,实际上是存在的,是给分配内存的。直接用子类对象调用不到被覆盖的父类方法或变量,但可以用super调用,如果是静态的,可以用父类名调用。

在方法中访问名字是a的变量,如果没有显式指定调用者,则系统查找a的顺序如何?
(1)查找a是否是该方法的局部变量
(2)查找a是否是该类的成员变量
(3)查找a是否是该类的父类的成员变量(一直追溯所有父类,直到Object)

class Parent{ public int a = 2;}
class Son extends Parent{ private int a = 3;/*覆盖掉了父类a变量*/}

class Test{
	public static void main(String[] args){
		
		Son son = new Son();
		//下面这句代码报错:a 在 Son 中是 private 访问控制
		//System.out.println(son.a);	
		
		System.out.println(((Parent)son).a);		
	}
}

构造器调用构造器之super调用直接父类构造器与this调用本类构造器综合

其实,创建子类的时候,顺着继承树往上走到头,首先创建的就是Object对象,首先调用的构造器就是Object的唯一的构造器——Object(),再原路返回依次创建各个父类对象,最后创建自己。

在一个构造器里面,通过super和this调用直接父类构造器和本类其他构造器的时候,注意super和this必须是第一行语句。


class Animal{
	public Animal(String name, int age){
		
		System.out.println("Animal两个参数构造器: "+"name="+name+", age="+age);
	}
}

class Person extends Animal{

	public Person(String name, int age, String where){
		super(name,age);
		System.out.println("Person三个参数构造器: "+"name="+name+", age="+age+", where="+where);
	}
}

class Student extends Person{
	public Student(){
		super("Tom", 18, "America");
		System.out.println("Student空参构造器");
	}
	public static void main(String[] args){
		new Student();
	}
}

输出:
Animal两个参数构造器: name=Tom, age=18
Person三个参数构造器: name=Tom, age=18, where=America
Student空参构造器

多态

Java引用变量有两个类型:编译时类型和运行时类型。编译时类型由声明该变量时用的类型决定,运行时类型由实际赋给该变量的对象决定。如果编译时和运行时的类型不一致,就是多态(polymorphism)。

class BaseClass{
	public int book = 666;
	
	public void base(){
		System.out.println("父类的普通方法");
	}

	public void test(){
		System.out.println("父类被子类覆盖的方法");
	}
}

class SubClass extends BaseClass{
	public String book = "hello";
	
	public void sub(){
		System.out.println("子类的普通方法");
	}
	public void test(){
		System.out.println("子类覆盖父类的方法");
	}

	public static void main(String[] args){
		BaseClass bc = new BaseClass();
		System.out.println(bc.book);
		bc.base();
		bc.test();

		SubClass sc = new SubClass();
		System.out.println(sc.book);
		sc.sub();
		sc.test();
	
		//多态
		BaseClass polymorphism = new SubClass();
		//下面代码输出666,运行时,输出的仍然是BaseClass类的成员变量
		System.out.println(polymorphism.book);
		polymorphism.base();
		//下面一句代码编译错误,编译时polymorphism是BaseClass类型的,没有sub方法
		//polymorphism.sub();
		polymorphism.test();
	}
	
}

注意:上面代码中的BaseClass polymorphism = new SubClass();产生多态。但是这句代码System.out.println(polymorphism.book);输出的是666,是BaseClass类的成员变量!可见,**对象的实例变量没有多态性!!**对象实例的方法才有多态性。通过引用变量访问其包含的实例成员变量时,系统总是试图访问他编译时类型所定义的成员变量。

类型转换

  • 基本类型之间的转换只能在数值类型(byte,short,char,int,long,float,double)之间进行。
  • 引用类型之间的转换只能在具有继承关系的来两个类之间进行。例如:Object o = "abc"; String s = (String)o;转换之前可以用instanceof判断下。
    if(x instanceof A){ A a = (A)x; }
  • instanceof运算符前一个操作数是引用类型变量,后一个操作数是一个类或接口,用来判断前面的对象是否是后面的类或子类的实例。前面的操作数的编译类型要么是后面的类类型,要么与后面的类具有父子继承关系,否则编译报错

class Hello{

	public static void main(String[] args){

		String s = "hello";
		//true
		System.out.println(s instanceof Object);
		//true
		System.out.println(s instanceof String);
		//下面这句代码编译报错:不兼容的类型: String无法转换为Math	
		//System.out.println((s instanceof Math));
	}
}

继承与组合

  • 继承是实现类复用的重要手段,但继承带来了一个最大的坏处:破坏父类的封装性。子类可以直接访问到父类的成员变量等内部信息,可以重写父类的方法等,破坏父类的封装性,子类和父类严重耦合。
  • 组合也是实现类复用的重要方式,采用组合方式实现类的复用能保证好的封装性。

为了保证父类的封装性,设计父类的时候通常遵循下面规则:

  • 尽量隐藏父类的内部数据,父类的成员变量为private访问类型
  • 父类中那些仅为辅助其他的工具方法,应该使用private访问控制符,让子类无法访问该方法;如果父类的方法需要被外部类调用,必须用public修饰,但又不希望子类重写该方法,可以用final修饰;如果希望父类的某个方法被子类重写,但不希望被其他类自由访问,可以用protected修饰
  • 尽量不要在父类构造器中调用要被子类重写的方法

下面代码会包空指针异常。因为在父类的构造器中调用了test方法,而test方法被子类覆盖,就调用了子类的test方法,name为空,使用name.length()引发空指针异常。


class Base{
	public Base(){
		test();
	}
	public void test(){
		System.out.println("Base");
	}
}

class Sub extends Base{
	private String name;
	public void test(){
		System.out.println(name.length());
	}

	public static void main(String[] args){
		new Sub();
	}
}


一个例子示范组合与继承
组合代码如下:

class Animal{
	public void breath(){
		System.out.println("I can breath...");
	}
}

class Bird{

	private Animal animal;
	public Bird(Animal animal){
		this.animal = animal;
	}

	public void breath(){
		this.animal.breath();
	}

	public void fly(){
		System.out.println("I can fly...");
	}

}

class Hello{

	public static void main(String[] args){
		Animal animal = new Animal();
		Bird bird = new Bird(animal);
		bird.breath();
		bird.fly();
		
	}
}

继承代码如下:

class Animal{
	public void breath(){
		System.out.println("I can breath...");
	}
}

class Bird extends Animal{

	public void fly(){
		System.out.println("I can fly...");
	}

}

class Hello{

	public static void main(String[] args){

		Bird bird = new Bird();
		bird.breath();
		bird.fly();
		
	}
}

初始化块

初始化块是Java类的第4种成员。初始化块分为普通初始化块(对象实例级别)和静态初始化块(类级别,用static修饰的初始化块)。静态初始化块是对类的静态成员,不能访问非静态成员。

  • 普通初始化块在构造器执行体执行之前执行,当多个构造器含有相同的代码,可以把这些代码抽取到初始化块中。其实本质上来说,初始化块就是构造器的一部分,在编译之后,初始化块就自动放到了每个构造器里面了。

下面代码输出2。对象创建后,先分配内存,默认初始化。然后初始化块初始化a=1,然后再初始化为2。如果调换{a=1;}和int a = 2;的顺序,输出1。

class Hello{
	
	{
		a = 1;	
	}
	
	int a = 2;
	
	public static void main(String[] args){
	
		System.out.println(new Hello().a);	
		
	}
}

初始化块的综合案例

class Root{

	static{
		System.out.println("Root静态初始化块");
	}
	
	{
		System.out.println("Root普通初始化块");
	}

	public Root(){
		System.out.println("Root空参构造器");
	}
}

class Mid extends Root{

	static{
		System.out.println("Mid静态初始化块");
	}

	{
		System.out.println("Mid普通初始化块");
	}

	public Mid(){
		System.out.println("Mid空参构造器");
	}

	public Mid(String msg){
		this();
		System.out.println("Mid带参构造器,参数: "+msg);
	}
}

class Leaf extends Mid{

	static{
		System.out.println("Leaf静态初始化块");
	}

	{
		System.out.println("Leaf普通初始化块");
	}

	public Leaf(){
		super("Hello");
		System.out.println("Leaf空参构造器");
	}
	
	public static void main(String[] args){
		new Leaf();
		new Leaf();
	}
}

输出:

Root静态初始化块
Mid静态初始化块
Leaf静态初始化块
Root普通初始化块
Root空参构造器
Mid普通初始化块
Mid空参构造器
Mid带参构造器,参数: Hello
Leaf普通初始化块
Leaf空参构造器
Root普通初始化块
Root空参构造器
Mid普通初始化块
Mid空参构造器
Mid带参构造器,参数: Hello
Leaf普通初始化块
Leaf空参构造器

猜你喜欢

转载自blog.csdn.net/ccnuacmhdu/article/details/82888301
今日推荐