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:

请我喝杯咖啡吧~

支付宝
微信