Feb 1997

Overtime and overdue


  • Home

  • Tags

  • Categories

  • Archives

  • Search

Java:前期绑定和后期绑定

Posted on 2019-08-31 Edited on 2019-09-01 In Java

概念

程序绑定

  绑定指的是一个方法的调用与方法所在的类(方法主体)关联起来。对Java来说,绑定分为静态绑定和动态绑定;或者叫做前期绑定和后期绑定。

静态绑定

  编译器在编译的时候就能解析的绑定叫做静态绑定或者前期绑定,Java当中的方法只有final,static和private方法是前期绑定。

为什么static,final和private方法总是静态绑定的?
  静态绑定在性能方面更好(不需要额外开销)。编译器知道这些方法不能被重写并且一直都可以被本地类的对象访问。因此编译器很轻松就能确定类的对象(肯定是本地类),所以被这种方法绑定是静态的。
例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class NewClass {
public static class superclass {
static void print(){
System.out.println("print in superclass");
}
}

public static class subclass extends superclass {
static void print() {
System.out.println("print in subclass.");
}
}

public static void main(String[] args) {
superclass A = new superclass();
superclass B = new subclass(); // 如果把此处的引用改成subclass,输出的就是"print in subclass"
A.print();
B.print();
}
}

输出:

1
2
print in superclass
print in superclass

分析:

  • 我们创建了一个subclass的对象和一个superclass的对象,并且引用了superclass
  • superclass的print方法是静态的,编译器知道它不会在子类中被重写,因此编译器在编译期间知道要调用哪种打印方法,因此不存在歧义

动态绑定

  在动态绑定中编译器不决定调用的方法,因为对象无法知道它是属于方法所在的那个类,还是属于那个类的导出类。重写是动态绑定的一个完美例子。在重写中子类和父类都有同样的方法。例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class NewClass {
// 和上一个代码的区别在于print()方法不再static了
public static class superclass {
void print(){
System.out.println("print in superclass");
}
}

public static class subclass extends superclass {
void print() {
System.out.println("print in subclass");
}
}

public static void main(String[] args) {
superclass A = new superclass();
superclass B = new subclass();
A.print();
B.print();
}
}

输出:

1
2
print in superclass
print in subclass

分析:

  • 这个代码中的方法不是静态的
  • 编译过程中,编译器不知道哪一个print方法被调用了,因为编译器只通过引用变量而不是对象类型来引用,于是绑定会被延迟到运行时而且因此对应版本的print会被根据对象类型调用

重点

  • private, final和静态成员(方法和变量)用静态绑定,然而对虚拟方法(Java方法默认为虚拟方法)绑定是在运行时基于运行时对象完成的
  • 静态绑定使用类型信息用于绑定然而动态绑定用对象来解析绑定
  • 重载方法通过静态绑定被解析(当有很多个同名方法时决定调用哪一个)然而重写方法用动态绑定

Q:
以下代码中发生了什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Animal
{
void eat()
{
System.out.println("Animal is eating");
}
}

class Dog extends Animal
{
void eat()
{
System.out.println("Dog is eating");
}
}

public static void main(String args[])
{
Animal a=new Animal();
a.eat();
}

A:
这个例子是一个动态绑定,因为a的类型实在运行时被确定的,于是相似的方法被调用了
现在假设有以下两个方法:

1
2
3
4
5
6
public static void callEat(Animal animal) {
System.out.println("Animal is eating");
}
public static void callEat(Dog dog) {
System.out.println("Dog is eating");
}

把main方法改成

1
2
3
4
5
public static void main(String args[])
{
Animal a = new Dog();
callEat(a);
}

输出会是Animal is eating,因为对callEat的调用是静态绑定,编译器只知道a是一种Animal。


参考

Geeksforgeeks
stackoverflow

Java:多态

Posted on 2019-08-30 Edited on 2020-05-26 In Java

这几天把《Java编程思想》中的第八章多态看了几遍,感觉相比其它两大特性(封装、继承)而言,多态更抽象,更难理解,运用起来更加灵活,当然这只是我目前而言的感受,在以后可能会有更深入的理解,现在能做的只有先把书中学到的东西先记录下来巩固一下,以后有新的收获再更新。

多态

多态通过分离做什么和怎么做,从另一角度将接口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能够创建可扩展的程序——即无论在项目最初创建时还是在需要添加新功能时都可以“生长”的程序。
“封装”通过合并特征和行为来创建新的数据类型。“实现隐藏”则通过将细节“私有化”把接口和实现分离开来。而多态的作用则是消除类型之间的耦合关系。多态方法调用允许一种类型表现出与其他相似类型之间的区别,只要它们都是从同一个基类导出来的,多态也称作动态绑定、后期绑定或运行时绑定。

向上整型

对象既可以作为它自己本身的类型使用,也可以作为它的基类型使用。把某个对象的引用视为对其基类型的引用的做法被称作向上转型。

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 enum Note {
MIDDLE_C, C_SHARP, B_FLAT;
}

class Instrument {
// 基类乐器,方法play()
public void play(Note n) {
// 输出"Instrument.play()"
}
}

public class Wind extends Instrument {
// 管乐器类继承乐器基类,并重写了play()方法
public void play(Note n) {
// 输出"Wind.play()" + n
}
}

public class Music {
public static void tune(Instrument i) {
// tune()方法:乐器对象调用play(),默认参数为Note.MIDDLE_C
i.play(Note.MIDDLE_C)
}
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute); //向上转型
}
}

输出:

1
Wind.play() MIDDLE_C

分析:
Music类中的tune()方法的参数列表中接受的参数是乐器类的对象,但是主函数中我们传入的对象是Wind类的,但是仍然输出了正确的结果,这就是向上转型的多态。从Wind向上转型到Instrument可能会”缩小“接口,但不会比Instrument的全部接口更窄。

仅接受基类

首先我们梳理一下上例:1.tune()方法接受的对象为Instrument类型对象;2.主函数中传入的对象为Wind类。
基于上述例子中我们可以想象,如果Instrument的子类有很多个的话,如果要实现同样的功能,会多出很多代码量并且代码看上去会非常的冗杂:我们要给各个子类单独创造对象,然后分别传入tune()方法。这样的代码会有很大一部分是类似的,因此我们会想办法:我们能不能写一个方法,仅接受基类作为参数,而不是子类。也就是说,如果我们不管子类的存在,编写的代码只与基类打交道会不会更好呢?
例:

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
35
36
37
38
39
40
public class Shape {
public void draw() {}
}

public class Circle extends Shape {
public void draw() {
// 输出"Circle.draw()"
}
}

public class Square extends Shape {
// 同上
}

public class Triangle extends Shape {
// 同上
}

public class RandomShapeGenerator {
private Random rand = new Random(47);
public Shape next() {
switch(rand.nextInt(3)) {
default:
case 0: return new Circle();
case 1: return new Square();
case 2: return new Triangle();
}
}
}

public class Shapes {
private static RandomShapeGenerator gen = new RandomShapeGenerator();
public static void main(String[] args) {
Shape[] s = new Shape[9];
for(int i = 0; i < s.length; i++)
s[i] = gen.next();
for(Shape shp : s)
shp.draw();
}
}

输出:

1
2
3
4
5
6
7
8
9
Triangle.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Circle.draw()

分析:三个子类(Triangle,Circle,Sauqre)继承自一个基类(Shape)。RandomShapeGenerator类中的next()方法负责生成不同子类对象并最终返回一个Shape类型的对象(这其中包括了一个向上转型的过程在return里)。因此当我们在主函数中调用next()方法时,只能获得一个Shape的引用,是不知道具体是哪个子类的,主函数中通过调用next()方法随机生成9个对象并且装进Shape类型的数组中,然后让数组中的每个对象执行draw()方法。通过分析输出我们很容易就能得出结论,与子类型有关的特定行为正确地发生了。
结论:在编译时,编译器不需要获得任何特殊信息就能进行正确的调用。(第一个代码的例子中,把Wind flute = new Wind()改成Instrument flute = new Wind()也能获得正确结果)

多层继承

之前举的例子都只有一层继承,除了Instrument外,都是它的子类。现在我们添加一层继承:

1
2
3
4
5
class Woodwind extends Wind {
void play(Note n) {
System.out.println("Woodwind.play() " + n);
}
}

主函数中执行:

1
2
Instrument woodwind = new Woodwind();
tune(woodwind);

输出:

1
FakeWind.play()MIDDLE_C

分析:多层继承并不会影响结果的正确输出。
总结:我们所做的代码修改,不会对程序中其他不应受到影响的部分产生破坏。换句话说,多态是一项让程序员“将改变的事物与未变的事物分离开来”的重要技术。

缺陷

覆盖私有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class PrivateOverride {
private void f() {
System.out.println("private f()");
}

public static void main(String[] args) {
PrivateOverride po = new Derived();
po.f();
}
}

class Derived extends PrivateOverride {
public void f() {
System.out.println("public f()");
}
}

输出:

1
private f()

分析:Derived类为PrivateOverride类的子类,并且尝试重写f()方法。主函数中用PrivateOverride引用指向一个Derived对象,根据之前的向上转型原则,我们期待的输出应该是”public f()”,但是输出是”private f()”。原因在于private方法被自动认为是final方法,而且对导出类是屏蔽的。因此,在这种情况下,Derived类中的f()方法就是一个全新的方法;既然基类中的f()方法在子类Derived中不可见,因此也不能被重载。
结论:只有非private方法才可以被覆盖;但是还需要密切注意覆盖private方法的现象,这时虽然编译器不会报错,但是也不会按照我们所期望的来执行。确切的说,在导出类中,对于基类中的private方法,最好采用不同的名字。
思考:如果想达到期望输出,可以把基类中的f()方法改成public或者private,这样子类就可以覆盖。或者把子类中的f()方法改名,并且在主函数中重新调用即可。

域与静态方法

只有普通的方法调用是可以多态的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Super {
public int field = 0;
public int getField() {return field;}
}

class Sub extends Super {
public int field = 1;
public int getField() {return field;}
public int getSupperField() { return super.field;}
}

public class FieldAccess {
public static void main(String[] args) {
Super sup = new Sub();
System.out.println("sup.field = " + sup.field + ", sup.getField() = " + sup.getField());
Sub sub = new Sub();
System.out.println("sub.field = " + sub.field + ", sub.getField() = " + sub.getField() + ", sub.getSupperFiled() = " + sub.getSupperField());
}
}

输出:

1
2
sup.field = 0, sup.getField() = 1
sub.field = 1, sub.getField() = 1, sub.getSupperFiled() = 0

分析:
在Java中,成员变量是静态绑定因为Java不允许对成员变量执行多态操作。
当Sub对象转型为Super引用时,任何域访问操作都将由编译器解析,因此不是多态的(多态是在运行过程中进行动态绑定)。在这个例子中,虚拟机为Super.field和Sub.field分配了不同的存储空间(在堆内存中)。这样,Sub实际上包含两个称为field的域。然而,在引用Sub中的field时所产生的默认域并非Super版本的域。因此,为了得到Super.field,必须显示的指明super.field。
总结:
以上这种访问方法无疑是非常易混淆的,因此实际开发中通常会将所有的域都设置成private。

另外一种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class StaticSupper {
public static String staticGet() {
return "Base staticGet()";
}
public String dynamicGet() {
return "Base dynamicGet()";
}
}
class StaticSub extends StaticSupper {
public static String staticGet() {
return "Derived staticGet()";
}
public String dynamicGet() {
return "Derived dynamicGet()";
}
}
public class StaticPolymorphism {
public static void main(String[] args) {
StaticSupper sup = new StaticSub();
System.out.println(sup.staticGet());
System.out.println(sup.dynamicGet());
}
}

输出:

1
2
Base staticGet()
Derived dynamicGet()

分析:基类中的staticGet()方法是静态方法,dynamicGet是非静态方法,从结果中可以看出子类并没有覆盖基类中的静态方法。
总结:静态方法是与类,而并非与单个的对象相关联的。

构造器和多态

构造器不具有多态性,因为它们实际上是static方法,只不过该static声明式隐式的。

构造器的调用顺序

基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。例:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Meal {
Meal() {
System.out.println("Meal");
}
}

class Bread {
Bread() {
System.out.println("Bread");
}
}

class Cheese {
Cheese() {
System.out.println("Cheese");
}
}

class Lettuce {
Lettuce() {
System.out.println("Lettuce");
}
}

class Lunch extends Meal {
Lunch() {
System.out.println("Lunch");
}
}

class PortableLunch extends Lunch {
PortableLunch() {
System.out.println("PortableLunch");
}
}

public class Sandwich extends PortableLunch{
Sandwich() {
System.out.println("Sandwich");
}
private Bread b = new Bread();
private Cheese c = new Cheese();
private Lettuce l = new Lettuce();

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

输出:

1
2
3
4
5
6
7
Meal
Lunch
PortableLunch
Bread
Cheese
Lettuce
Sandwich

分析&总结:从输出中验证结论,调用构造器要遵照下面的顺序:

  1. 调用基类构造器。这个步骤会不断地反复地递归下去,首先是构造这种层次结构的根,然后是下一层导出类,等等,直到最低层的导出类。
  2. 按声明顺序调用成员的初始化方法。
  3. 调用导出类构造器的主体。

Q. What is the difference between compile-time polymorphism and runtime polymorphism?

There are two types of polymorphism in java:
1) Static Polymorphism also known as compile time polymorphism
2) Dynamic Polymorphism also known as runtime polymorphism

Example of static Polymorphism

Method overloading is one of the way java supports static polymorphism. Here we have two definitions of the same method add() which add method would be called is determined by the parameter list at the compile time. That is the reason this is also known as compile time polymorphism.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SimpleCalculator
{
int add(int a, int b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
}
public class Demo
{
public static void main(String args[]) {

SimpleCalculator obj = new SimpleCalculator();
System.out.println(obj.add(10, 20));
System.out.println(obj.add(10, 20, 30));
}
}

Output:

1
2
30
60

Runtime Polymorphism (or Dynamic polymorphism)

It is also known as Dynamic Method Dispatch. Dynamic polymorphism is a process in which a call to an overridden method is resolved at runtime, thats why it is called runtime polymorphism.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ABC {

public void myMethod() {
System.out.println("Overridden Method");
}
}
public class XYZ extends ABC {

public void myMethod() {
System.out.println("Overriding Method");
}
public static void main(String args[]) {
ABC obj = new XYZ();
obj.myMethod();
}
}

Output:

1
Overriding Method

Q. Can you achieve Runtime Polymorphism by data members?

No, we cannot achieve runtime polymorphism by data members. Method is overridden not the data members, so runtime polymorphism can’t be achieved by data members.

Q. Overloading vs Overridden

Overloading occurs when two or more methods in one class have the same method name but different parameters. Overriding means having two methods with the same method name and parameters (i.e., method signature). One of the methods is in the parent class and the other is in the child class.

Java:is-a和has-a

Posted on 2019-08-29 In Java

之前讨论过Java的代码复用问题,其中提到了组合、继承和代理三中复用方法。在进一步了解这三种方法的使用场景时,首先需要鉴别类之间的关系。

is-a

is-a:该关系依赖于继承。如果A是B,那么B就是A的基类。
比如土豆是蔬菜,公交车是载具,灯泡是电器等等。继承的一个性质就是继承是单向的,比如说公寓是建筑物,但是不是所有建筑物都是公寓。

has-a

has-a:这种关系称为组合。如果A中有B,那么B就是A的组成部分。
一个类的实例有指向另一个类的索引或者同一个类的其他实例。比如汽车有引擎,狗有尾巴等等。

Java:代码复用

Posted on 2019-08-27 Edited on 2019-10-14 In Java

介绍

  复用代码是Java众多引人注目的功能之一。但要想成为极具革命性的语言,仅仅能够复制代码并对之加以改变是不够的,它还必须能够做更多的事情。
  实现代码复用的第一种方法:只需在新的类中产生现有类的对象。由于新的类是由现有类的对象所组成,所以这种方法称为组合。
  实现代码复用的第二种方法:按照现有类的类型来创建新类。无需改变现有类的形式,采用现有类的形式并在其中添加新代码。这种方式称为继承。

组合

  将对象引用置于新类中即可。例如我现在有两个类:

1
2
3
4
5
6
7
8
9
10
11
class Wheel {
void run(){
System.out.println("run"); // 车轮类的功能是run
}
}

class Light {
void lit() {
System.out.println("lit"); // 车灯类的功能是lit
}
}

  我现在有一个新的类Car,需要同时有run和lit的功能,此时应当使用组合,这样我就可以在Car类中同时拥有两个类的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Car {
private Wheel wheel;
private Light light;

public Car(Wheel wheel, Light light) {
this.wheel = wheel;
this.light = light;
}

public void operation() {
wheel.run();
light.lit();
}

public static void main(String[] args) {
Wheel wheel = new Wheel();
Light light = new Light();
Car car = new Car(wheel, light);
car.operation();
}
}

继承与代理

  Java中的继承是一个类继承另一个类的性质的过程。比如一个新的类我们称之为衍生类或子类,继承了之前就存在的我们称之为父类、超类或者基类的类的属性和行为。
  代理仅仅是把任务传递给别的类。

  • 代理可以是继承的替代品
  • 代理意味着你将其他类的对象作为实例变量传递信息给指定的实例
  • 很多情况下代理比继承好因为代理让你去考虑你所传递的每个信息,因为实例属于一个已知的类而不是一个新的类,并且不强迫你去接受父类的所有方法:你可以只提供有用的方法
  • 代理可以被看作对象之间的关系因为一个对象转发相应的方法调用给另一个对象,这叫做代理
  • 代理的主要优势是运行时间的灵活性——代理可以很容易地改变运行时间。但是和继承不同,代理并不被主流的面向对象语言所支持,而且它不易于动多态。
    例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class RealPrinter{
    void print() {
    System.out.println("The delegate");
    }
    }

    class Printer {
    RealPrinter p = new RealPrinter();

    void print() {
    p.print();
    }
    }
    public class Tester {
    public static void main(String[] args) {
    Printer printer = new Printer();
    printer.print();
    }
    }

输出:

1
The delegate

  当使用代理时,只需要调用一些必须要用的类,不用关心是怎么实现的,只需要知道你调用的那个类知道要做什么。
  同样的代码用继承来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class RealPrinter {
void print() {
System.out.println("Printing Data");
}
}

class Printer extends RealPrinter {
void print() {
super.print();
}
}
public class Tester {
public static void main(String[] args) {
Printer printer = new Printer();
printer.print();
}
}

输出:

1
Printing Data

什么时候该用什么?
这里举几个例子来说明什么时候用代理什么时候用继承:
假设你的类叫B,是A的子类,那么如果:

  • 你想表达(is-a)关系,用继承
  • 你想把类直接传递到一个现有的需要A的API那么就用继承
  • 你想改进A,但是A已经是终点并且不能再被继承了,那你就要用组合和代理

个人理解

  首先想一想,组合是怎么工作的。我们把对象引用放在新的类里,这就意味着我们可以通过组合语法,在一个类中拥有多个指向其他类的引用。
  其次想一想继承是如何工作的,继承通过extends关键词,可以接过父类中所有的属性和方法,但是由于Java是不支持多继承的,所以一个类只能继承一个父类。
  根据特性我们可以总结一下继承和组合的优缺点:
继承
优点:

  • 支持扩展;
  • 被复用的代码易于修改

缺点:

  • 父类的实现细节都暴露给了子类,破坏了封装性;
  • 当父类代码修改时,子类也要修改,增加了维护的难度;
  • 子类缺乏独立性,与父类的耦合度高;
  • 不支持动态拓展,在编译期就决定了父类。

组合
优点:

  • 被包括的对象内部实现细节对外不可见,封装性好;
  • 整体类与局部类松耦合,相互独立;
  • 支持扩展;
  • 每个类只负责一项业务;
  • 支持动态扩展,可在运行时根据具体对象选择不同类型的组合对象(扩展性比继承好)

缺点:

  • 创建整体类对象时,需要创建所有局部类对象。导致系统对象很多。

  关于代理,《Java编程思想》中对代理有这样一句描述:这是继承与组合之间的中庸之道。在代理类中可以创建某功能的类,调用类的一些方法获得该类的部分特性。比如说我有一个飞机类,飞机有向各个方向运动的方法,还有发射导弹的方法。我可以通过在代理类中只调用(就像组合)运动方法的方法来避免暴露发射导弹的方法,同时代理类中暴露了其他运动的方法(就像继承)。

参考资料

csdn

Java:protected提供包访问权限

Posted on 2019-08-25 Edited on 2019-08-29 In Java

问题描述

  Java类中的属性在不指定访问权限,即private、protected或者public的情况下被称为“默认访问模式”,该模式下,只允许在同一个包中访问。
  当需要继承的类来自另外一个包(packet)时,如果父类中的方法是私有的或者是默认访问模式(即未指定的),那么子类是不能继承的,
父类代码:

1
2
3
4
5
6
7
8
9
10
package 01;

public class Cookie {
public Cookie(){
System.out.println("Cookie constructor");
}
void bite(){
System.out.println("bite");
}
}

子类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package 02;
import 01.Cookie; // 导入父类所在的包

public class ChocolateChip extends Cookie{
public ChocolateChip(){
System.out.println("ChocolateChip constructor");
}
public void chomp(){
bite();
}

public static void main(String[] args) {
ChocolateChip x = new ChocolateChip();
x.chomp();
}
}

错误信息:

1
2
3
Error:(9, 9) java: 找不到符号
符号: 方法 bite()
位置: 类 com.demo.xliu294.ChocolateChip

  假设我们现在希望子类可以继承来自另外一个包的父类中的方法,可以将父类中的该方法设置为public或者protected,但是如果把它指定为public的话,所有的人都有了访问权限,这样是不合适的。但是如果改成protected的话,子类中的bite()就可以访问了。

总结

  如果创建了一个新包,并自另一个包中继承类,那么唯一可以访问的成员就是源包中的public成员。(当然,如果在同一个包内执行继承工作,就可以操纵所有的拥有包访问权限的成员。)有时,基类的创建者会希望有某个特定成员,把对它的访问权限赋予派生类而不是所有类。这就需要protected来完成这一工作。protected也提供包访问权限,也就是说,相同包内的其他类可以访问protected元素。

参考

《Java编程思想》

Java踩坑:初始化顺序

Posted on 2019-08-25 Edited on 2019-09-06 In Java

问题说明

求以下输出:

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
public class Window {
Window(int marker){
System.out.println("Window(" + marker + ")");
}
}

public class House {
Window w1 = new Window(1);
House(){
System.out.println("House()");
w3 = new Window(33);
}
Window w2 = new Window(2);
void f(){
System.out.println("f()");
}
Window w3 = new Window(3);
}

public class OrderOfInitialization {
public static void main(String[] args) {
House h = new House();
h.f();
}
}

错误答案

1
2
3
4
5
6
House()
Window(3)
Window(33)
Window(1)
Window(2)
f()

错误思路

从主函数出发创建一个House类对象h,构造函数会在创建对象的同时执行,找到House类的构造函数,先输出House(),因为w3是个没被声明的变量,所以先运行了Window w3 = new Window(3)输出Window(3)然后再完成构造函数,输出Window(33),然后按照顺序完成以下输出。

正确答案

1
2
3
4
5
6
Window(1)
Window(2)
Window(3)
House()
Window(33)
f()

分析

在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化。在House类中,故意把几个Window对象的定义散布到各处,以整明它们全都会在调用构造器或其他方法之前得到初始化。此外,w3在构造期内再次被初始化。

总结

其实在意识到如果构造函数先执行的话w3 = new Window(33)这一句中的w3是一个没有被声明的变量时就觉得不太对,但还是没有找出正确的思路。后来在尝试别的情况时把这一句注释掉了,输出顺序还是不变,只是少了一句Window(33)。


2019-09-06

今天在学习多态的过程中又遇到了一个初始化顺序的问题,代码如下:

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
class Glyph {
public void draw() {
System.out.println("Glyph.draw()");
}
Glyph() {
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}

class RoundGlyph extends Glyph {
private int radius = 1;
RoundGlyph(int r) {
radius = r;
System.out.println("RoundGlyph.RoundGlyph().radius = " + radius);
}
public void draw() {
System.out.println("RoundGlyph.draw().radius = " + radius);
}
}

public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
}

错误答案:

1
2
3
4
Glyph() before draw()
RoundGlyph.draw().radius = 1
Glyph() after draw()
RoundGlyph.RoundGlyph.radius = 5

错误思路:
首先调用基类构造器,考虑到新创建的对象是RoundGlyph()类的,我认为此处的draw()方法应该已经被覆盖,应该调用子类的draw方法,radius的值在子类中被初始化为1,因此得到了第二行的输出。最后再调用子类构造器的主体,输出第四行。
正确答案:

1
2
3
4
Glyph() before draw()
RoundGlyph.draw().radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph.radius = 5

正确思路:除了第二行radius的值以外都对了。初始化的实际过程是:1)在其他任何事物发生之前,将分配给对象的存储空间初始化为二进制的0。2)如前所述那样调用基类构造器。此时,调用被覆盖后的draw()方法(需要在调用RoundGlyph构造器之前调用),由于步骤1的缘故,我们此时会发现radius的值为0。3)按照声明的顺序调用成员的初始化方法。4)调用导出类的构造器主体。

Java:隔离岛

Posted on 2019-08-24 Edited on 2019-08-29 In Java

介绍

  在Java中,对象的销毁由GC(垃圾收集器)模块负责,并且那些没有被引用的对象是可以被垃圾收集的,垃圾收集器能够识别这类的对象。
  隔离岛:
  - 对象1引用对象2和对象2引用对象1.对象1和对象2都不被任何其他对象引用。
  - 通常来说隔离岛是一组互相引用但不被应用程序中其它活动的对象引用的对象。严格来说,即使一个没有被引用的对象也是一个隔离岛。
例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
Test i; // 类里定义了一个对象引用
public static void main(String[] args) {
Test t1 = new Test();
Test t2 = new Test();

t1.i = t2;
t2.i = t1;
t1 = null;
t2 = null;
System.gc();
}
@Override
protected void finalize() throws Throwable{
System.out.println("Finalize method called");
}
}

输出:

1
2
Finalize method called
Finalize method called

分析:
  在销毁一个对象之前,垃圾收集器对一个对象最多调用一次finalize方法,在上例中finalize方法被调用两次的理由是因为两个对象都有资格被垃圾收集。这是因为我们在执行t2=null之后没有任何对t1和t2的外部引用。
  我们只有内部的互相引用(在Test类的实例变量i中)。我们没有办法去调用两个对象的实例变量。所以不能再次调用任何一个对象。
  我们在Test类里面创建了一个属性Test i,主函数中创建了两个Test类型的对象,并且用两个引用t1和t2去指向这两个对象,然后通过t1.i去访问t2,通过t2.i去访问t1,直到t2.i = t1: 每个对象都有外部引用t1和t2。这里需要记住的是t1和t2仅仅是引用,真正的对象在图中是以圈表示的,下图中t1指向一个对象,这个对象中有属性i,通过t1这个对象的属性i指向另一个对象,也就是t2指向的对象,反之亦然:

  t1 = null: 此时引用t1已经不指向任何对象,但是对象还是存在的,只不过通过t2.i可以到达之前t1指向的对象,通过t2可以直接访问另一个对象:

  t2 = null: 分析过程和上一步相同,此时没有办法到达任何一个对象:

  现在,两个对象都有资格被垃圾收集,因为我们没有办法调用他们了。这通常被称为隔离岛。

参考资料

GeeksforGeeks

Java:GC机制

Posted on 2019-08-23 Edited on 2020-05-26 In Java

介绍

  • 在 C/C++中,编程者需要同时负责对象的创建与销毁。通常情况下编程者会忘记销毁无用的对象。由于这种忽视,在某个时间点可能会没有足够的内存空间去创建新的对象从而导致整个程序因为内存溢出而终止。
  • 在Java中,编程者不需要关心哪个对象不会再被用上。垃圾收集器会销毁这些对象。
  • 垃圾收集器是守护线程的最好例子因为它总是在后台运行。
  • 垃圾收集器的主要目的是通过摧毁不可到达的对象从而释放内存堆。

    重要术语

  1. 不可到达的对象(Unreachable objects): 如果一个对象没有任何指向它的索引那么我们称之为不可到达的对象。同样注意到属于隔离岛一部分的对象也是不可到达的。
    1
    2
    3
    4
    Integer i = new Integer(4);
    // 通过i中的索引,这个新的整型对象是可到达的
    i = null;
    // 那个整型对象不可到达了

img

  1. 垃圾收集的合格性(Eligibility for garbage collection):如果一个对象是不可到达的,那么就可以被垃圾收集。在上图中,在i = null;之后堆空间中的整型对象4就可以被垃圾收集了。

使对象可以被垃圾收集的方法

  • 即使编程者不需要负责摧毁无用的对象,但还是推荐把不再需要的对象变得不可到达。
  • 通常有四种方法让对象变得可以被垃圾收集。
    1. 让引用变量为空
    2. 重新分配引用变量
    3. 在方法内部创建对象
    4. 隔离岛

请求JVM运行垃圾收集器的方法

  • 一旦我们让对象可以被垃圾收集,它可能不会立即被垃圾收集器销毁。无论何时JVM运行垃圾收集程序,只有对象会被销毁。但是当JVM运行垃圾收集器,我们不能预计。
  • 我们也可以请求JVM运行垃圾收集器。有以下两种方法:
    1. 使用System.gc()方法: System类包含静态方法gc()来请求JVM运行垃圾收集器。
    2. 使用Runtime.getRuntime().gc()方法: Runtime类允许应用程序与运行应用程序的JVM进行交互。于是通过使用它的gc()方法,我们可以请求JVM运行垃圾收集器。
      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
      // Java程序描述请求JVM去运行垃圾收集器
      public class Test
      {
      public static void main(String[] args) throws InterruptedException
      {
      Test t1 = new Test();
      Test t2 = new Test();

      //让引用变量为空
      t1 = null;

      //请求JVM运行垃圾收集器
      System.gc();

      //让引用变量为空
      t2 = null;

      //请求JVM运行垃圾收集器
      Runtime.getRuntime().gc();
      }

      @Override
      // finalize method is called on object once
      // before garbage collecting it
      protected void finalize() thorws Throwable
      {
      System.out.println("Garbage collector called");
      System.out.println("Object garbage collected:" + this);
      }
      }

注意:

  1. 无法保证以上两种方式中的任何一种绝对会运行垃圾处理器。
  2. 调用两种方法在效率上是等价的。

Finalization

  • 在销毁对象之前,垃圾收集器对对象调用finalize()方法来执行清理动作。一旦finalize()方法完成,垃圾收集器销毁对象。
  • fianlize()方法出现在Object类里,该方法的访问修饰符为protected,原型如下:
    1
    protected void finalize() throws Throwable

根据我们的需求,我们可以重写finalize()方法来执行我们的清理动作比如关闭数据库的连接。

注意:

  1. finalize()是由垃圾收集器调用的而不是JVM。虽然垃圾收集器是JVM的一个模型。
  2. Object类finalize()方法的执行为空,因此如果要部署系统资源或者执行其他清理的话需要重载这个方法。
  3. finalize()方法不会对一个对象调用多次。
  4. 如果finalize()方法抛出了一个不可捕捉的异常,异常会被忽略而且对象的终结会被终止。

垃圾收集器的使用

假设要写一个程序去计算公司中工作的员工数(除去实习生),你需要垃圾收集器来完成这个程序。
任务描述:
写一个程序创建一个叫做Employee的类包含以下的数据成员:

  1. ID,存储分配给每个员工的独一无二的id
  2. 员工的姓名
  3. 员工的年龄
    同时提供以下方法:
  4. 一个带参构造函数用来初始化姓名和年龄,ID需要在构造器中被初始化
  5. 一个show()方法去展示ID,姓名和年龄
  6. 一个showNextId()去展示每个员工的下一个ID
    作为一个没有垃圾收集器知识的初学者会这样写:
    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
    35
    36
    37
    public class Employee {
    private int ID;
    private String name;
    private int age;
    private static int nextId=1;

    public Employee(String name, int age){
    this.name = name;
    this.age = age;
    this.ID = nextId++;
    }
    public void show(){
    System.out.println("Id="+ID+"\nName="+name+"\nAge="+age);
    }
    public void showNextId(){
    System.out.println("Next employee id of "+ID" will be="+nextId);
    }
    }

    public class test {
    public static void main(String[] args) {
    Employee E = new Employee("GFG1", 56);
    E.showNextId();
    Employee F = new Employee("GFG2", 45);
    F.showNextId();
    Employee G = new Employee("GFG3", 25);
    G.showNextId();
    {
    //用块来保存实习生
    Employee X = new Employee("GFG4", 23);
    X.showNextId();
    Employee Y = new Employee("GFG5", 21);
    Y.showNextId();
    }
    G.showNextId();
    }
    }

输出:

1
2
3
4
5
6
Next employee id of 1 will be=2
Next employee id of 2 will be=3
Next employee id of 3 will be=4
Next employee id of 4 will be=5
Next employee id of 5 will be=6
Next employee id of 3 will be=6

类的定义中nextId属性为静态,被所有对象共享。第一个G.showNextId()输出的是4,第二个G.showNextId()输出的是6。
如何获得获得正确的输出(即让第二个G.showNextId()输出4):
现在垃圾收集器会看到两个空闲的对象,现在去减少nextId的值,垃圾收集器只有在编程者在类中重载的情况下才会调用方法finalize()。正如之前提到的,我们需要向垃圾收集器发起请求,我们要在子块的中写下一下三个步骤:

  1. 将引用变量设为空
  2. 调用System.gc();
  3. 调用System.runFinalization();
    现在计算员工(除了实习生)的正确代码是:
    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
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    public class Employee {
    private int ID;
    private String name;
    private int age;
    private static int nextId=1;

    public Employee(String name, int age){
    this.name = name;
    this.age = age;
    this.ID = nextId++;
    }
    public void show(){
    System.out.println("Id="+ID+"\nName="+name+"\nAge="+age);
    }
    public void showNextId(){
    System.out.println("Next employee id of "+ID" will be="+nextId);
    }
    // 供gc调用
    protected void finalize(){
    --nextId;
    }
    }

    public class test {
    public static void main(String[] args) {
    Employee E = new Employee("GFG1", 56);
    E.showNextId();
    Employee F = new Employee("GFG2", 45);
    F.showNextId();
    Employee G = new Employee("GFG3", 25);
    G.showNextId();
    {
    //用块来保存实习生
    Employee X = new Employee("GFG4", 23);
    X.showNextId();
    Employee Y = new Employee("GFG5", 21);
    Y.showNextId();
    X = Y = null; // 设置为空
    System.gc(); //调用垃圾收集器
    System.runFinalization(); // 执行finalize
    }
    G.showNextId();
    }
    }

输出:

1
2
3
4
5
6
Next employee id of 1 will be=2
Next employee id of 2 will be=3
Next employee id of 3 will be=4
Next employee id of 4 will be=5
Next employee id of 5 will be=6
Next employee id of 3 will be=4

参考资料

GeeksforGeeks


Q. How do you force garbage collection to occur at a certain point?
A. Call System.forceGc()
B. Call System.gc()
C. Call System.requireGc()
D. None of the above

Answer: D
While you can suggest to the JVM that it might want to run a garbage collection cycle, the JVM is free to ignore your suggestion.

Java:Static关键字

Posted on 2019-08-21 Edited on 2019-08-29 In Java

Static介绍与用法

  通常来说,当创建类时,就是在描述那个类的对象的外观与行为。除非用new创建那个类的对象,否则,实际上并未获得任何对象。执行new来创建对象时,数据存储空间才被分配,其方法才供外界调用。
  有两种情形用上述方法是无解的。一种情形是,只想为某特定域分配单一存储空间,而不去考虑究竟要创建多少对象,甚至根本就不创建任何对象。另一种情形是,希望某个方法不与包含它的类的任何对象关联在一起。也就是说,即使没有创建对象,也能调用这个方法。
  通过static关键字可以满足这两方面的需要。当声明一个事物是static时,就意味着这个域或方法不会与包含它的那个类的任何对象实例关联在一起。所以,即使从未创建某个类的对任何对象,也可以调用其static方法或访问其static域。通常,你必须创建一个对象,并用它来访问数据或方法。因为非static域和方法必须知道他们一起运作的特定对象。

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
35
36
37
38
public class StaticTest {
static int i = 47;
int j = 47;
static void incrementi(){
i++;
}
void incrementj(){
j++;
}
}


public class test {
public static void main(String[] args) {

StaticTest st1 = new StaticTest();
StaticTest st2 = new StaticTest();

System.out.println("st1.i: " + st1.i);
System.out.println("st2.i: " + st2.i);

st1.incrementi();
System.out.println("st1.incrementi()后的st1.i: " + st1.i);
System.out.println("st1.incrementi()后的st2.i: " + st2.i);

st1.incrementj();
System.out.println("st1.incrementj()后的st1.j: " + st1.j);
System.out.println("st1.incrementj()后的st2.j: " + st2.j);
}
}

//Output:
st1.i: 47
st2.i: 47
st1.incrementi()后的st1.i: 48
st1.incrementi()后的st2.i: 48
st1.incrementj()后的st1.j: 48
st1.incrementj()后的st2.j: 47

以上代码中i是静态变量,incrementi()是静态方法。创建两个对象并且只对其中一个执行incrementi()方法得出的输出中,两个对象的i属性都加了1。incrementj()不是静态方法,所以st1执行incrementj()后,st2的j值不变。

  将字段或方法设定为static,即使创建了两个StaticTest对象,StaticTest.i也只有一份存储空间,这两个对象公像一个i,上例中的st1.i和st2.j指向同一存储空间。

引用static对象时可以通过类名直接引用:

1
StaticTest.i++

或者因为incrementi()是一个静态方法,也可以直接通过类调用:

1
StaticTest.incrementi()

  尽管当static作用于某个字段时,肯定会改变数据创建的方式(因为一个static字段对每个类来说都只有一份存储空间,而非static字段则是对每个对象有一个存储空间),但是如果static作用于某个方法,差别却没有那么大。static方法的一个重要用法就是在不创建任何对象的前提下就可以调用它,这一点对定义main()方法很重要,这个方法是运行一个应用时的一个入口点。

个人理解

  对于Static的理解我个人认为重点是认识到Static的字段,不管创建多少个对象,字段都是指向同一个存储空间,也就是说都是同时改变的。

参考资料

《JAVA编程思想第四版》


Q:为什么main函数中不能定义static变量?
A:只有类才存在静态的变量,方法只能对静态变量的操作,不能在方法内试图定义静态变量。

Django:模型访问

Posted on 2019-08-19 Edited on 2019-08-29

Manager属性

  Manager是Django模型最重要的属性,通过使用Manager模型才可以操作数据库。默认情况下,Django会为每一个模型提供一个名为objects的Manager实例。Manager属性只能通过模型类访问。

自定义Manager类

1
2
3
4
5
6
7
8
class Blog(models.Model):
'''模型的定义'''
name = models.CharField(max_length=100)
tagline = models.TextField()
blog = models.Manager()

def __str__(self):
return self.name

上例中的blog = models.Manager()自定义Manager类(之前的情况是不加这一句的),可以通过Blog.blog.all()查询数据,返回的结果是:

1
<QuerySet [<Blog: Beatles Blog>, <Blog: Scott Blog>]>

改进一下:

1
2
3
qs = Blog.blog.all()
for b in qs:
print(b.name)

返回的结果是:

1
2
Beatles Blog
Scott Blog

执行SQL语句

  1. Manager.raw()
    语法:Manager.raw(raw_query, params=None, translations=None)
    针对同样的Blog模型
    例:
    1
    2
    3
    blogs = Blog.blog.raw("select * from tst_blog")  # 第一个blog是我在模型中将Manager()命名为blog,tst_blog是表
    for b in blogs:
    print(b.name)

输出结果:

1
2
Beatles Blog
Scott Blog

注意:如果不能返回数据则会抛出异常

  1. django.db.connection
      django.db.connection对象提供了数据库连接操作,使用connection.cursor()方法可以得到一个游标对象,cursor.execute(sql,[paramas])方法用于执行指定的SQL语句。使用cursor.fetchone()或者cursor.fetchall()方法可以得到一个或全部结果。

    表与表之间的关系

    多对一

      在关系型数据库中通常使用外键来表示多对一关系,Django模型中的ForeignKey字段就是模型的外键,写在“多”的那个模型下,其第一个参数对应相关联的类,例如一个问题可以有多个选项:
    1
    2
    3
    4
    5
    class Question(models.Model):
    content = models.CharField(max_length=128)

    class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE) # on_delete=models.CASCADE属性必须设置,表示级联删除

  以上代码在生成数据库时,Django会自动在Choice所对应的表中添加字段question_id作为外键。
访问方法:
前向查询:通过 Choice中的外键查询Question

1
2
c = Choice.objects.get(pk=1)
c.question

反向查询:通过Question查询Choice

1
2
q = Question.objects.get(pk=1)
q.choice_set.all()

多对多

  假设公司里一个员工属于一个或多个部门,每个部门有一个或多个员工,为了实现该组织架构,使用ManyToManyField字段类型:

1
2
3
4
5
6
class Department(models.Model):
name = models.CharField(max_length=50)

class Employee(models.Model):
departments = models.ManyToManyField(Department) # 多对多字段名用复数
name = models.CharField(max_length=50)

  注意:可以在任一个模型中定义ManyToManyField,但是不能两个都定义。
对于多对多关系,Django会在数据库中额外创建一张关系表,关系表的命名规则是:应用程序名_模型1名_模型2名s,在上例中生成的表名为app_employee_departments
访问方法(类似反向查询):

1
2
d = Department.objects.get(id=3)
d.Employee_set.all()

一对一

  使用OneToOneField表示一对一关系,第一个参数是模型名。这种关系用的少。

1…9101112
Feb 1997

Feb 1997

112 posts
4 categories
24 tags
© 2020 Feb 1997