Fork me on GitHub

继承的细节--protected

继承和多态概念还有一些相关的细节,具体包括:

  • 构造方法
  • 重名与静态绑定
  • 重载和重写
  • 父子类型转换
  • 继承访问权限(protected) now!
  • 可见性重写
  • 防止继承(final)

​

除了public/private之外,还有一种可见性介于中间的修饰符protected。

protected表示虽然不能被外部操作访问,但可被子类访问,还可被同一个包中的其他类访问(不管其他类是否是该类的子类)。

​

举个栗子,基类Base代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Base {
protected int currentStep;

protected void step1() {
}

protected void step2() {
}

public void action() {
this.currentStep = 1;
step1();
this.currentStep = 2;
step2();
}
}

含有protected修饰符的是变量currentStep和方法step1、step2,还对外提供了方法action。

子类一般不重写action,只重写step1和step2。同时,子类可以直接访问currentStep查看进行到了哪一步。

子类Child的代码如下:

1
2
3
4
5
6
7
8
9
public class Child extends Base{
protected void step1 () {
System.out.println("child step "+this.currentStep);
}

protected void step2 () {
System.out.println("child step "+this.currentStep);
}
}

使用子类的main方法代码如下:

1
2
3
4
public static void main(String[] args) {
Child child = new Child();
child.action();
}

输出结果为:

1
2
child step 1
child step 2

子类通过重写protected方法step1和step2,来修改对外的行为。

​

这种思路和设计在设计模式中被称之为模板方法,action就是一个模板方法,它定义了实现的模板,而具体实现由子类提供。

模板方法在很多框架中由广泛的应用,这是使用protected的一个常用场景。

继承的细节--父子类型转换

继承和多态概念还有一些相关的细节,具体包括:

  • 构造方法
  • 重名与静态绑定
  • 重载和重写
  • 父子类型转换 now!
  • 继承访问权限(protected)
  • 可见性重写
  • 防止继承(final)

​

根据《从图形处理类看继承和多态》一章可知,子类型的对象可以赋值给父类型的引用变量。

但父类型的变量不一定能成功赋值给子类型的变量,向下转型不一定成功。

栗子如下:

1
2
Base base = new Child();
Child child = (Child)base;

Child child = (Child)base就是将变量base的类型强制转换为Child并赋值为child。
因为base的动态类型就是child,所以向下转型没问题。

1
2
Base base = new Base();
Child child = (Child)base;

上面代码,在运行时会抛出错误,错误为类型转换异常。

​

一个父类的变量,能否转换为一个子类的变量,取决于这个父类变量的动态类型(即引用的对象类型)是不是这个子类或这个子类的子类。

​

给定一个父类的变量,通过instanceof关键字,能知道它到底是不是某个子类的对象,从而安全的进行类型转换。

1
2
3
public static boolean canCast(Base b) {
return b instanceof Child;
}

canCast函数返回Base类型变量b是否可以转换为Child类型。

instanceof前面是变量,后面是类,返回值为true时表示变量引用的对象是该类或其子类的对象,false则反之。

继承的细节--重载和重写

继承和多态概念还有一些相关的细节,具体包括:

  • 构造方法

  • 重名与静态绑定

  • 重载和重写 now!

  • 父子类型转换

  • 继承访问权限(protected)

  • 可见性重写

  • 防止继承(final)

​

重载和重写

重载=方法名称相同,但参数签名不同(参数个数或类型或顺序不同)。

重写= 子类重写父类相同参数签名的方法。

​

举一个栗子,父类代码如下:

1
2
3
4
5
6
public class Base {
public int sum(int a,int b) {
System.out.println("base_int_int");
return a+b;
}
}

父类定义了sum(int a,int b)方法。

子类代码如下:

1
2
3
4
5
6
public class Child extends Base{
public long sum(long a,long b) {
System.out.println("child_long_long");
return a+b;
}
}

子类定义了sum(long a,long b)方法。

​

调用代码如下:

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
Child child = new Child();
int a = 2;
int b = 3;
child.sum(a,b);
}
}

虽然每个sum方法都是兼容的,int类型可以自动转型为long,当只有一个方法的时候,那个方法就会被调用。

但现在有多个方法可用,子类的sum方法参数类型虽然兼容但是不完全匹配,而父类的sum方法参数类型是完全匹配的。

所以父类的sum方法被调用了,输出结果为:

1
base_int_int

​

如果父类将其中一个int参数改为long参数,代码如下:

1
2
3
4
5
6
public class Base {
public long sum(int a,long b) {
System.out.println("base_int_long");
return a+b;
}
}

再运行程序,调用的依然是父类的sum函数,代码如下:

1
base_int_long

虽然子类和父类的两个方法的参数类型都不完全匹配,但是相比之下,还是父类的sum方法更匹配一些,至少有一个int参数。

​

现在修改一下子类代码,让它的参数类型和父类一致:

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
Base base = new Child();//静态类型是Base,动态类型是Child
int a = 2;
int b = 3;
base.sum(a,b);
}
}

输出结果为:

1
child_int_long

说明这一回调用的是子类的sum函数。

​

从上面的这些栗子中,我们可以得知——当有多个重名函数的时候,在决定要调用哪个函数的过程中,首先是按照参数类型进行匹配的。换句话说,寻找在所有重载函数中最匹配的。然后才看变量的动态类型,进行动态绑定。

继承的细节--重名与静态绑定

继承和多态概念还有一些相关的细节,具体包括:

  • 构造方法
  • 重名与静态绑定 now!
  • 重载和重写
  • 父子类型转换
  • 继承访问权限(protected)
  • 可见性重写
  • 防止继承(final)

​

重名

对于private变量和方法,它们只能在类内被访问,访问的也永远是当前类的对象。

​

对于public变量和方法,在类内访问的是当前类的,但子类可以通过super.明确指定访问父类的。

在类外,则要看访问变量的静态类型,静态类型是父类,则访问父类的变量和方法;静态类型是子类,则访问子类的变量和方法。

​

接下来举个栗子。父类Base代码如下:

1
2
3
4
5
6
7
8
9
//父类
public class Base {
public static String s = "static_base";
public String m = "base";

public static void staticTest() {
System.out.println("base static: "+s);
}
}

Base类定义了一个public静态变量s,一个public实例变量m、一个静态方法staticTest。

子类Child代码如下:

1
2
3
4
5
6
7
8
public class Child extends Base {
public static String s = "child_base";
public String m = "child";

public static void staticTest() {
System.out.println("child static: "+s);
}
}

子类定义了和父类重名的变量m、s和方法staticTest。

对于一个子类对象,它就有了两份变量和方法。在子类内部访问的时候,访问的是子类的。子类变量和方法隐藏了父类对应的变量和方法。

​

调用代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
Child c = new Child();
Base b = c;

System.out.println(b.s);
System.out.println(b.m);
b.staticTest();

System.out.println(c.s);
System.out.println(c.m);
c.staticTest();
}

创建一个子类对象new Child(),然后将该对象分别赋值给子类引用变量c和父类引用变量b。

再通过b和c分别引用变量和方法。

输出结果为:

1
2
3
4
5
6
static_base
base
base static: static_base
child_base
child
child static: child_base

通过b(静态类型Base)访问时,访问的是Base的变量和方法。通过c(静态类型Child)访问时,访问的是Child的变量和方法。

这叫做静态绑定,即访问绑定到变量的静态类型。

实例变量、静态变量、private方法,都是静态绑定的。

继承的细节--构造方法

继承和多态概念还有一些相关的细节,具体包括:

  • 构造方法 now!

  • 重名与静态绑定

  • 重载和重写

  • 父子类型转换

  • 继承访问权限(protected)

  • 可见性重写

  • 防止继承(final)

​

cop.kujiale|owt.khrd|pdl.i18n|service.message-service

构造方法

构造方法与类同名且没有返回值。

构造方法的语句格式:

1
2
3
public 构造方法名() {
//初始化代码
}

只能在对象实例化(new 对象)的时候被调用。

子类可以通过super(…)调用父类的构造方法。如果子类没有通过super(…)调用,则会自动调用父类的默认构造方法。

如果父类没有默认构造方法,如下所示:

1
2
3
4
5
6
7
//父类
public class Base {
private String member;
public Base(String member) {
this.member = member;
}
}

上方代码中的类只有一个带参数的构造函数,没有默认构造方法。

这个时候,它的任何子类都必须在构造方法中通过super(…)调用Base的带参数的构造方法。

如下所示,否则Java会提示编译错误。

1
2
3
4
5
6
//子类
public class Child extends Base{
public Child(String member) {
super(member); //调用Base的带参构造方法
}
}

​

​

避免父类的构造方法调用可重写的方法

如果在父类构造方法中调用了可被重写的方法,则可能会出现意想不到的结果。

父类代码如下:

1
2
3
4
5
6
7
8
9
//父类
public class Base {
public Base() {
test(); //构造函数中调用test方法
}

public void test() {
}
}

子类代码如下:

1
2
3
4
5
6
7
8
9
10
11
//子类
public class Child extends Base{
private int a = 123;

public Child() {
}

public void test() {
System.out.println(a);
}
}

子类有一个实例变量a,a初始赋值为123。子类重写了test方法,要输出a的值。

使用代码如下:

1
2
3
4
public static void main(String[] args) {
Child c = new Child();
c.test();
}

输出结果:

1
2
0
123

第一次输出0,是在new过程中输出的。

new过程中,首先初始化父类Base,父类构造方法调用test(),test被子类重写了,就会调用子类的test方法。

子类方法访问子类实例变量a,这个时候子类的实例变量的赋值语句和构造方法还没有执行,所以输出的默认值是0。

​

通过上面的例子,可以得出结论——在父类构造方法中调用可被子类重写的方法,是一种不好的实践,容易引起混淆,应该只调用private的方法。

从图形处理类看继承和多态

各类图形.jpg

上图有一些基本的图形。接下来,用以下类来定义图形:

  • 父类Shape,表示图形

  • 类Circle,表示圆

  • 类Line,表示直线

  • 类ArrowLine,表示带箭头的直线

​

图形Shape类有一个表示颜色的属性,和一个表示绘制的方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//父类:图形类
public class Shape {

private static final String DEFAULT_COLOR = "black";
private String color;

public Shape() {
this(DEFAULT_COLOR);
}

public Shape(String color) {
this.color = color;
}

public String getColor() {
return color;
}

public void setColor(String color) {
this.color = color;
}

public void draw() {
System.out.println("draw shape");
}
}

​

圆circle类继承自Shape,但包括了Shape没有的中心点和半径属性,以及额外的方法area,用于计算面积。

此外又重写了draw方法。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Circle extends Shape {
//中心点
private Point center;
//半径
private double r;

public Circle(Point center,double r) {
this.center = center;
this.r = r;
}

@Override
public void draw() {
System.out.println("draw circle at "+center.toString()+" with r "
+r+", using color : "+getColor());
}

public double area() {
return Math.PI*r*r;
}
}

​

直线Line类继承Shape类,它有两个点,有一个获取长度的方法,并且重写了draw方法。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Line extends Shape {
private Point start;
private Point end;

public Line(Point start,Point end,String color) {
super(color);
this.start = start;
this.end = end;
}

public double length() {
return start.distance(end);
}

public Point getStart() {
return start;
}

public Point getEnd() {
return end;
}

@Override
public void draw() {
System.out.println("draw line from "+ start.toString() + " to "
+end.toString()+",using color "+super.getColor());
}
}

​

带箭头直线ArrowLine类继承自Line类,多了表示两端是否有箭头的两个属性,也重写了draw方法。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ArrowLine extends Line{
//起点是否有箭头
private boolean startArrow;
//终点是否有箭头
private boolean endArrow;

public ArrowLine(Point start,Point end,String color,
boolean startArrow,boolean endArrow) {
super(start,end,color);
this.startArrow = startArrow;
this.endArrow = endArrow;
}

@Override
public void draw() {
super.draw();
if (startArrow) {
System.out.println("draw start arrow");
}
if (endArrow) {
System.out.println("draw end arrow");
}
}
}

super.draw();表示调用父类的draw()方法。

​

使用继承的一个好处是可以统一处理不同子类型的对象。

比如说,设计一个图形管理ShapeManager类,它负责管理画板上的所有图形对象并负责绘制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ShapeManager {
//图形最大个数为100
private static final int MAX_NUM = 100;
private Shape[] shapes = new Shape[MAX_NUM];
private int shapeNum = 0;

public void addShape(Shape shape) {
if (shapeNum < MAX_NUM) {
shapes[shapeNum++] = shape;
}
}

public void draw() {
for (int i=0;i<shapeNum;i++) {
shapes[i].draw();
}
}
}

如上方代码,ShapeManager使用一个数组保存所有的shape,在draw方法中调用每个shape的draw方法。

​

接下来使用ShapeManager:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
ShapeManager manager = new ShapeManager();

manager.addShape(new Circle(new Point(4,4),3)); //添加圆形
manager.addShape(new Line(new Point(2,3),new Point(3,4),"green")); //添加直线
manager.addShape(new ArrowLine(new Point(1,2),new Point(5,5),"black",false,false)); //添加箭头线

manager.draw();
}

新建了三个图形,分别是一个圆、直线、箭头线,然后加到ShapeManager中,再调用manager的draw方法。

在addShape方法中,参数Shape shape,声明的类型是Shape,而实际的类型则分别是Circle、Line、ArrowLine。子类对象赋值给父类引用变量,叫做向上转型,转型=转换类型,向上转型=转换为父类类型。

变量Shape可以引用任何Shape子类类型的对象,称为多态,即一种类型的变量,可引用多种实际类型对象。

对于变量shape,它有两个类型,类型Shape,我们称之为shape的静态类型;类型Circle/Line/ArrowLine,我们称之为shape的动态类型。

在ShapeManager的draw方法中,shapes[i].draw()调用的是其对应动态类型的draw方法,这称之为方法的动态绑定。

​

为什么要有多态和动态绑定呢?创建对象的代码 (ShapeManager以外的代码)和操作对象的代码(ShapeManager本身的代码),经常不在一起,操作对象的代码往往只知道对象是某种父类型,也往往只需要知道它是某种父类型就可以了。

多态和动态绑定是计算机程序中的一种重要思维方式,使得操作对象的程序不需要关注对象的实际类型,从而可以统一处理不同对象,但又能实现每个对象的特有行为。

类的继承

计算机程序经常使用类之间的继承关系来表示对象之间的分类关系。

在继承关系中,有父类(基类)和子类(派生类)。比如动物类Animal和狗类Dog,Animal是父类,Dog是子类。

子类继承了父类的属性和行为,父类有的属性和行为,子类都有。但子类可以增加子类特有的属性和行为,某些父类有的行为,子类的实现方式可能与父类也不完全一样。

​

​

Object

在Java中,所有类都有一个父类,就是Object。

Object没有定义属性,但定义了一些方法,如下方代码:

1
2
3
4
5
6
7
8
9
equals(Object obj):boolean -Object
getClass():Class<?> -Object
hashCode():int -Object
notify():void -Object
notifyAll():void -Object
toString():String -Object
wait():void -Object
wait(long timeout):void -Object
wait(long timeout,int nanos):void -Object

以上方法可以被所有类直接使用。

子类可以重写父类的方法,以反应自己的不同实现。

方法前面放一个**@Override**,代表是重写的方法。

下图是Object的toString()方法, 返回的是类名和内存地址(hashcode)。

Object类的toString方法.png

下图经过重写toString()方法,返回的是m。

重写的toString方法.png

​

Java使用extends关键字标明继承关系,一个类最多只能有一个父类。

子类不能直接访问父类的私有属性和方法。

除了私有的属性和方法外,子类继承了父类的其他属性和方法。

假设父类Animal类如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//父类:动物类
public class Animal {

private String name;
private int age;

public void setName(String name) {
this.name = name;
}

public void setAge(int age) {
this.age = age;
}
}

包含私有属性name和age,公共方法setName和setAge。

子类Cat类不能直接调用name和age,但可以调用setName和setAge。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//子类:猫类
public class Cat extends Animal {

private String owner;

public Cat(String owner,int age,String name) {
this.owner = owner;
this.setAge(age);
super.setName(name); //super关键字
}


public static void main(String[] args) {
Cat cat = new Cat("me",1,"shaded");
}
}

super关键字指代父类,可用于调用父类构造方法,访问父类方法和变量。

类的组合

基础类

String是Java API中的一个类,表示多个字符。它内部是一个char的数组,提供了若干方法用于方便操作字符串。

String可以用一个字符串常量初始化,必须是双引号。

​

Date是Java API中的一个类,表示日期和时间。它内部是一个long类型的值,提供了若干方法用于操作日期和时间。

1
Date now = new Date();

如上面代码,用无参的构造方法创建一个Date对象,这个对象就表示当前时间。

​

​

图形类

先扩展一下《类的定义》一章中的Point类,为它添加一个“计算改点到另一个点的距离”的方法。

1
2
3
4
public double distance(Point p) {
return Math.sqrt(Math.pow(x-p.getX(),2)+
Math.pow(y-p.getY(),2));
}

设计一个表示线 - Line的类,其中类的属性含有Point类。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Line {
private Point start;
private Point end;

public Line(Point start,Point end) {
this.start = start;
this.end = end;
}

public double length() {
return start.distance(end);
}
}

Line由两个Point组成,在创建Line时需要两个Point。

使用这个类的代码如下:

1
2
3
4
5
6
7
public static void main(String[] args) {
Point start = new Point(2,3);
Point end = new Point(3,4);

Line line = new Line(start,end);
System.out.println(Line.length());
}

​

​

电商概念

尝试用类描述一下电商系统中的一些基本概念。

电商系统中最基本的有产品、用户和订单:

  • 产品:有产品唯一Id、名称、描述、图片、价格等属性。

  • 用户:有用户名、密码等属性。

  • 订单:有订单号、下单用户、选购产品列表及数量、下单时间、收货人、收货地址、联系电话、订单状态等属性。

产品Product类的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Product {
//唯一id
private String id;

//产品名称
private String name;

//产品图片链接
private String pictureUrl;

//产品描述
private String description;

//产品价格
private double price;
}

用户User类的代码如下:

1
2
3
4
public class User {
private String name;
private String password;
}

订单条目OrderItem类来描述单个产品及选购的数量,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class OrderItem {
//购买产品
private Product product;

//购买数量
private int quantity;

public OrderItem(Product product,int quantity) {
this.product = productl;
this.quantity = quantity;
}

//合计价格
public double computePrice() {
return product.getPrice()*quantity;
}
}

最后是包含全部属性的订单Order类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Order {
//订单号
private String id;

//购买用户
private User user;

//购买产品列表及数量
private OrderItem[] items;

//下单时间
private Date createtime;

//收货人
private String receiver;

//收货地址
private String address;

//联系电话
private String phone;

//订单状态
private String status;

public double computeTotalPrice() {
double totalPrice = 0;
if(items != null) {
for(OrderItem item : items) {
totalPrice+=item.computePrice(); //计算总金额
}
}
}
}

Order类引用了User类,以及一个订单条目的数组items,定义了一个计算总价的方法。

​

​

引用自己的类

一个类定义中还可以引用它自己。

比如想要描述人以及人之间的血缘关系,用Person类表示一个人,它的实例成员包括其父母孩子,这些成员也都是Person类型。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Person {
//姓名
private String name;

//父亲
private Person father;

//母亲
private Person mother;

//孩子数组
private Person[] children;

public Person(String name) {
this.name = name;
}
}

使用代码如下:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
Person daZhongMa = new Person("大仲马");
Person xiaoZhongMa = new Person("小仲马");

xiaoZhongMa.setFather(daZhongMa);
daZhongMa.setChildren(new Person[]{xiaoZhongMa});

System.out.println(xiaoZhongMa.getFather().getName());
}

上面代码先创建了大仲马,然后创建了小仲马,接着调用xiaozhongma的setFather()和dazhongma的setChildren(),设置了父子关系。

栈-地址 栈-内容 堆-地址 堆-内容 含义
0x8000 0x1000 0x1000 小仲马 xiaoZhongMa.name
0x1004 0x1010 xiaoZhongMa.father
0x1008 null xiaoZhongMa.mother
0x100C null xiaoZhongMa.children
0x8008 0x1010 0x1010 大仲马 daZhongMa.name
0x1014 null daZhongMa.father
0x1018 null daZhongMa.mother
0x101C 0x1000 daZhongMa.children

​

​

互相引用的类

定义两个类MyFile和MyFolder,分别表示文件和文件夹。

文件和文件夹都有名称、创建时间、父文件夹,根文件夹没有父文件夹,文件夹还有子文件列表和子文件夹列表。

文件MyFile类代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyFile {
//文件名称
private String name;

//创建时间
private Date createtime;

//文件大小
private int size;

//上级目录
private MyFolder parent;

public int getSize() {
return size;
}
}

文件夹MyFolder类的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class MyFolder {
//文件夹名称
private String name;

//创建时间
private Date createtime;

//上级文件夹
private MyFolder parent;

//包含的文件
private MyFile[] files;

//包含的子文件夹
private MyFolder[] subFolders;

public int totalSize() {
int totalSize = 0;
if (files != null) {
for(MyFile file : files) {
totalSize+=file.getSize();
}
}
if(subFolders != null) {
for(MyFolder folder : subFolders) {
totalSize+=folder.totalSize(); //计算子文件夹的size
}
}

return totalSize;
}
}

MyFile引用了MyFolder,MyFolder也引用了MyFile,两者组合互相引用。

类的定义

类既是函数的容器,也用来表示自定义数据类型。

​

所谓自定义数据类型,就是除了八种基本类型以外的其他类型,用于表示和处理基本类型以外的其他数据。

一个数据类型,由其包含的属性以及该类型可以进行的操作组成。属性又可以分为是类型本身具有的属性,还是一个具体数据具有的属性。同样,操作也可以分为是类型本身可以进行的操作,还是一个具体数据可以进行的操作。

由此,一个数据类型就主要由四部分组成:

  • 类型本身具有的属性,通过类变量体现

  • 类型本身可以进行的操作,通过类方法体现

  • 类型实例具有的属性,通过实例变量体现

  • 类型实例可以进行的操作,通过实例方法体现

​

类方法

static表示类方法,又称静态方法。与类方法相对的是实例方法,实例方法没有static修饰符,必须通过实例或者叫对象调用。而类方法可以直接通过类名进行调用,不需要创建实例。

​

类变量

类型本身具有的属性通过类变量体现,经常用于表示一个类型中的常量。

比如Math类中定义的两个数学常量:

1
2
public static final double E = 2.7182818284590452354;
public static final double PI = 3.14159265358979323846;

static表示是类变量。与类变量相对的是实例变量,没有static修饰符。

final表示变量被赋值后,就不能再修改了。

实例变量

实例变量表示具体的实例所具有的属性。与基本类型对比, int a;这个语句,int就是类型,a就是实例。

​

实例方法

实例方法表示具体的实例可以进行的操作。

在实例方法中,有一个隐含的参数(this),这个参数就是当前操作的实例自己,它直接操作方法和变量。

  • 类方法只能访问类变量,但不能访问实例变量。可以调用其他的类方法,但不能调用实例方法。
  • 实例方法既能访问实例变量,也可以访问类变量。既可以调用实例方法,也可以调用类方法。

class变量和数组变量本身不存储数据,而只是存储实际内容的位置,它们都称为引用类型的变量。

​

通过对象来访问和操作其内部的数据是一种基本的面向对象思维。

​

定义一个合格的类,实例变量是private属性,通过实例方法来操作实例变量。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Point {
private int x;
private int y;

public void setX(int x) {
this.x = x;
}

public void setY(int y) {
this.y = y;
}

public int getX() {
return x;
}

public int getY() {
return y;
}

public double distance() {
return Math.sqrt(x*x + y*y);
}
}

this关键字表示当前实例,实例方法中隐含的参数就是this。

在语句this.x=x;中,this.x表示实例变量x,右边的x表示方法参数中的x。

使用这个类的代码如下:

1
2
3
4
5
6
public static void main(String[] args) {
Point p = new Point();
p.setX(2);
p.setY(3);
System.out.println(p.distance());
}

将对实例变量的直接访问改为了方法调用。

​

引入构造方法

1
2
3
4
5
6
7
8
public Point() { //调用第二个构造方法
this(2,3);
}

public Point(int x,int y) {
this.x=x;
this.y=y;
}

上面代码中的两个方法就是构造方法,构造方法可以有多个。

构造方法的特殊之处:

  • 名称是固定的,与类名相同。

  • 不能有返回值。

this不仅可以访问实例变量,还可以在构造方法中调用其他方法,这个this调用必须放在第一行。

栈

函数调用方和函数本身就如何传递参数、如何返回结果、如何跳转指令等问题,使用内存来存放这些数据,并且就如何存放和使用这些数据达成一个一致的协议或约定。

这个约定在各种计算机系统中都是类似的,存放这些数据的内存有一个相同的名字,叫栈。

​

栈是一块内存,顺序是先进后出,类似于一个书堆,越晚放上的书越先拿走。

栈的最下面称为栈底,最上面成为栈顶。

往栈底放数据,称为入栈;往栈顶取数据,成为出栈。

栈一般是从高位地址向低位地址扩展,所以栈底的内存地址是最高的,栈顶的内存地址是最低的。

​

计算机系统主要使用栈来存放函数调用过程中需要的数据,包括参数、返回地址,函数内定义的局部变量也放在栈中。

​

1
2
3
4
5
6
7
8
9
10
11
public class Sum {
public static int sum(int a,int b) {
int c = a + b;
return c;
}

public static void main(String[] args) {
int d = Sum.sum(1,2);
System.out.println(d);
}
}

上面的代码,概念上:main函数调用了sum函数,计算1+2,然后输出计算结果。

从栈的角度:

(1)当程序在main函数调用Sum.sum函数之前,栈的情况如下表格

地址 内容 函数
0x7FF4
0x7FF8
0x7FFC d main
0x8000 args main

栈主要存放了两个变量args(从控制台接收到的参数)和d。

(2)在程序执行到Sum.sum的函数内部,准备return c之前,栈的情况如下表格。

地址 内容 函数
0x7FEC 3(c) sum
0x7FF0 栈中保存的返回地址 sum
0x7FF4 2(b) sum
0x7FF8 1(a) sum
0x7FFC d main
0x8000 args main

main函数调用Sum.sum时,首先将参数1和2入栈,然后将返回地址入栈,接着跳转到sum函数,在sum函数内部,为局部变量c分配一个空间,而参数变量a和b则直接对应于入栈的数据1和2。在返回之前,返回值3保存到了专门的返回值存储器中。

(3)调用return后,程序会跳转到栈中保存的返回地址,sum函数相关的数据会出战,从而又变回(1)中的状态。

地址 内容 函数
0x7FF4
0x7FF8
0x7FFC d main
0x8000 args main

(4)main的下一条指令是根据函数返回值给变量d赋值,返回值从专门的返回值存储器中获得。

​

​

函数中的基本数据类型参数和函数内定义的基本数据类型变量,都分配在栈中。这些变量只有在函数被调用的时候才分配,而且在调用结束后就被释放了。

而数组和对象类型,它们都有两块内存,一块存放实际的内容,一块存放实际内容的地址。实际的内容一般不是分配在栈上的,而是分配在堆中,但存放地址的空间是分配在栈上的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ArrayMax {
public static int max(int min,int[] arr) {
int max = min;
for(int a : arr) {
if(a > max) {
max = a;
}
}
return max;
}

public static void main(String[] args) {
int[] arr = new int[]{2,3,4};
int ret = max(0,arr);
System.out.println(ret);
}
}

上面的代码,概念上:main函数新建一个数组int[]{2,3,4},然后调用函数max计算0和数组中元素的最大值,最后打印输出最大值。

从栈的角度:

栈-地址 栈-内容 函数 堆-地址 堆-内容
0x7FE8 max(4) max 0x1000 2
0x7EFC 栈中保存的返回地址 max 0x1004 3
0x7FF0 0x1000(arr) max 0x1008 4
0x7FF4 0(min) max
0x7FF8 ret main
0x7FFC 0x1000(arr) main
0x8000 args main

对于数组arr,在栈中存放的是实际内容的地址0x1000,存放地址的栈空间会随着入栈分配,出栈释放。

存放实际内容的堆空间,在函数结束、栈空间没有变量指向它的时候,java系统会自动进行垃圾回收,从而释放这块空间。

​

栈的空间不是无限的。栈空间过深,系统就会抛出错误——java.lang.StackOverflowError,即栈溢出错误。

  • Copyrights © 2021 Silver Shaded
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信