面向对象

1
2
3
4
5
6
7
String s1 = "java";
String s2 = "java";
String s3 = new String("java");
String s4 = new String("java");
//s1==s2:t
//s1==s3:f
//s3==s4:f

s1==s2:java维护常量池,s1与s2会指向同一个对象。

而且s2="java1"也会构造新的对象,而不会赋值


下面来说一下,java中的隐藏和覆盖的概念。我们知道,当子类继承父类时,除了继承父类所有的成员变量和成员方法之外,还可以声明自己的成员变量和成员方法。那么,如果父类和子类的成员变量和方法同名会发生什么?假设有一个父类Father和一个子类Son。父类有一个成员变量a=0;有一个静态成员变量b=0;有一个成员方法a,输出0;有一个静态成员方法b,输出0。子类分别重写这些变量和方法,只是修改变量的值和方法的输出,全部改为1. 我们再声明一个静态类型是父类,动态类型是子类的引用:

Father father=new Son();

通过这个引用访问子类的变量和调用子类的方法,那么,会有以下结论:

1、所有的成员变量(不管是静态还是非静态)都只进行静态绑定,所以JVM的绑定结果会是:直接访问静态类型中的成员变量,也就是父类的成员变量,输出0.

2、对于静态方法,也是只进行静态绑定,所以JVM会通过引用的静态类型,也就是Father类,来进行绑定,结果为:直接调用父类中的静态方法,输出0.

3、对于非静态方法,会进行动态绑定,JVM检查出引用father的动态类型,也就是Son类,绑定结果为:调用子类中的静态方法,输出1.

对于1和2这两种情况,子类继承父类后,父类的属性和静态方法并没有被子类抹去,通过相应的引用可以访问的到。但是在子类中不能显示地看到,这种情况就称为隐藏。

而对于3这种情况,子类继承父类后,父类的非静态方法被子类重写后覆盖上去,通过相应的引用也访问不到了(除非创建父类的对象来调用)。这种情况称为覆盖。

总结一下,就是,子类继承父类后:

父类的成员变量只会被隐藏,而且支持交叉隐藏(比如静态变量被非静态变量隐藏)。父类的静态方法只会被静态方法隐藏,不支持交叉隐藏。父类的非静态方法会被覆盖,但是不能交叉覆盖。

继承对存储分配的影响

替换原则

指对于类A和类B,如果B是A的子类,那么在任何情况下都可以用类B来替换类A

可替换性是面向对象编程中一种强大的软件开发技术。可替换性的意思是:变量声明时指定的类型不必与它所容纳的值类型相一致。这在传统的编程语言中是不允许的,但在面向对象的编程语言中却常常出现

三种分配方案

1.最小静态空间分配:只分配基类所需的存储空间。

2.无论基类还是派生类,都分配可用于所有合法的数值的最大的存储空间。

3.只分配用于保存一个指针所需的存储空间。在运行时通过堆来分配数值所需的存储空间,同时将指针设为相应的合适值。

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Window {
public:
virtual void oops();
private:
int height;
int width;
};
class TextWindow : public Window {
public:
virtual void oops();
private:
char * contents;
int cursorLocation;
};

由替换原则,我们可能使用一个Window对象指向其子类对象。我们创建一个Window对象时,如何进行内存分配?

最小静态空间分配

C++使用最小静态空间分配策略。比如这种情况:

初始时给win分配了父类大小的存储空间,然后使用一个指针指向了一个子类的内存地址(注意这两种创建对象方式的区别,一种是直接在栈内存中创建,另一种是在栈内存中定义了一个指向堆内存空间的这种对象的指针),最后将这个子类复制给Window对象。

显然内存空间是不够的,我们只能复制到子类中的父类空间大小的部分,而剩下的子类部分就无法复制。

C++保证变量只能调用定义于Window类中的方法,不能调用定义于TextWindow类中的方法。定义并实现于Window类中的方法无法存取或修改定义于子类中的数据,因此不可能出现父类存取子类的情况。

对于指针(引用)变量:当消息调用可能被改写的成员函数时,选择哪个成员函数取决于接收器的动态数值。

对于其他变量:关于调用虚拟成员函数的绑定方式取决于静态类(变量声明时的类),而不取决于动态类(变量所包含的实际数值的类)。

最大静态空间分配

分配变量值可能使用的最大存储空间。

问题:对象大小?对整个程序扫描?

要求太严格了,因此在主要的面向对象编程语言中,都没有使用这种方法。

运行时的堆分配(动态分配)

堆栈中不保存对象值。堆栈通过指针大小空间来保存标识变量,数据值保存在堆中。指针变量都具有恒定不变的大小,变量赋值时,不会有任何问题。

第3章 面向对象程序设计

封装

封装是一种数据隐藏技术,用户只能看到封装界面上的信息,对象内部对用户是不可见的。实际上使用方法将类的数据隐藏起来,控制用户对类的数据(域、属性)修改和访问的权限。被封装的对象之间是通过传递消息来进行联系的。一个消息由三部分组成:

  • 消息的接受对象

  • 接收对象要采取的方法

  • 方法需要的参数

类变量和类方法

静态成员函数

  • 不能访问非静态成员

  • 无this/不能使用this引用

  • 构造和析构函数不能为静态成员

也就是说,静态中只能有静态,非静态没有限制。

内部类

将一个类的定义放在另一个类的定义内部(Java内部类。C++嵌套类)

  • 语义差别
    • Java内部类被连接到外部类的具体实例上,并且允许存取其实例和方法
    • C++仅是命名手段,限制和内部类相关的特征可视性

内部类是非常有用的特性,因为它允许你把一些逻辑相关的类组织在一起,并控制位于内部的类的可视性

  • 内部类的作用:
    • 可以无条件地访问外围类的所有元素
    • 实现隐藏
    • 可以实现多重继承
    • 通过匿名内部类来优化简单的接口实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Outer
{
private int size ; //外部类的成员变量
private String thoughts = "My outer thoughts";

class Inner //声明内部类
{
String innerThoughts = "My inner thoughts";
//内部类的成员变量

void doStuff() //内部类的成员方法
{
// inner object has its own "this"
System.out.println( innerThoughts );

// and it also has a kind of "outer this"
// even for private data of outer class
System.out.println(thoughts);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class TestMe
{
public static void main( String args[] )
{
// instantiate me, the outer object
Outer o = new Outer();

// Inner i = new Inner();
// NO! Can't instantiate Inner by itself!

Outer.Inner i = o.new Inner();
// now I have my special inner object
i.doStuff();
// OK to call methods on inner object
}
}
//也可以这样 Outer.Inner i = new Outer().new Inner();

对象的创建及内存分配

给对象赋值时则会修改堆内存中的值

对象引用传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ClassDemo05 {
public static void main(String args[]) {
Person per1 = null; // 声明per1对象
Person per2 = null; // 声明per2对象
per1 = new Person(); // 只实例化per1一个对象
per2 = per1 ;// 把per1的堆内存空间使用权给per2
per1.name = "张三";// 设置per1对象的name属性内容
per1.age = 30; // 设置per1对象的age属性内容
// 设置per2对象的内容,实际上就是设置per1对象的内容
per2.age = 33;
System.out.print("per1对象中的内容 --> ") ;
per1.tell(); // 调用类中的方法
System.out.print("per2对象中的内容 --> ") ;
per2.tell();
}
}

对象拷贝

在 Java 中,除了基本数据类型(元类型)之外,还存在 类的实例对象 这个引用数据类型。而一般使用 『 = 』号做赋值操作的时候。对于基本数据类型,实际上是拷贝的它的值,但是对于对象而言,其实赋值的只是这个对象的引用,将原对象的引用传递过去,他们实际上还是指向的同一个对象。

而浅拷贝和深拷贝就是在这个基础之上做的区分,如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行了引用的传递,而没有真实的创建一个新的对象,则认为是浅拷贝。反之,在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量,则认为是深拷贝。

所以到现在,就应该了解了,所谓的浅拷贝和深拷贝,只是在拷贝对象的时候,对 类的实例对象 这种引用数据类型的不同操作而已。

总结来说:

1、浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。

2、深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

直接调用clone()方法产生的效果是:先在内存中开辟一块和原始对象一样的空间,然后原样拷贝原始对象中的内容。对基本数据类型,这样的操作是没有问题的,但对非基本类型变量,我们知道它们保存的仅仅是对象的引用,这也导致clone后的非基本类型变量和原始对象中相应的变量指向的是同一个对象。就导致了浅拷贝。

constfinal区别

对于Dart:(应该不会考这个)

final修饰的变量,必须在定义时将其初始化,其值在初始化后不可改变; const用来定义常量。

它们的区别在于,constfinal更加严格。final只是要求变量在初始化后值不变,但通过final,我们无法在编译时(运行之前)知道这个变量的值;而const所修饰的是编译时常量,我们在编译时就已经知道了它的值,显然,它的值也是不可改变的。


Final仅断言相关变量不会赋予新值,并不能阻止在对象内部对变量值进行改变


Java中的final有三种主要用法:

(1)修饰变量:

final变量是不可改变的,但它的值可以在运行时刻初始化,也可以在编译时刻初始化,甚至可以放在构造函数中初始化,而不必在声明的时候初始化,所以下面的语句均合法:

1
2
3
final int i = 1; // 编译时刻
final int i2 = (int)(Math.Random() * 10); //运行时刻
final int i3; //构造函数里再初始化

final经常和static一起用,这种用法类似C++的常量,在Java中很常见,比如 static final i = 10; 但这里同样也是允许运行时刻初始化的。

(2)修饰类对象:

而如果修饰类对象,并不表示这个对象不可更改,而是表示这个这个变量不可再赋成其它对象,这就比较像 C++的 Class const * p了(这样表明这个指向该Class的指针p不能再指向其他对象,指针常量,但是该对象中的值是可以修改的(const Class *p 是常量指针,任何成员变量都不能修改))

1
2
3
final Value v = new Value(); 
v = new Value(); //不允许!
v.some_method(); //允许

(3)修饰方法:

final修饰的方法是不能被重载的,类似于类中的private方法,所以private方法默认是final的

大致说就是变量不可修改(基本数据类型值不能修改,类类型引用不能修改),方法不可重载,类不可继承,

java反射

可以参考 https://blog.csdn.net/a745233700/article/details/82893076

Java 到底是值传递还是引用传递

可以参考 https://www.zhihu.com/question/31203609

对象的动态与静态类型

静态类型检查:基于程序的源代码来验证类型安全的过程;

动态类型检查:在程序运行期间验证类型安全的过程;

假设B的父类是A,那么A a = new B();,静态类型是A,动态类型是B。

Java使用静态类型检查在编译期间分析程序,确保没有类型错误。基本的思想是不要让类型错误在运行期间发生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A {
A me() {
return this;
}

public void doA() {
System.out.println("Do A");
}
}

class B extends A {
public void doB() {
System.out.println("Do B");
}
}

首先,调用new B().me()将返回什么呢?A对象还是B?

me()方法被声明将返回A对象,所以在编译期间,编译器只知道它返回A对象。然而,它在运行期间却返回了B对象,因为B继承了A的方法返回了自己

如下代码行是非法的,即使方法doB()是被B对象调用的。问题在于它的引用类型是A,在编译期间,编译器不知道它的真实类型,所以将它当做A类型

1
2
//illegal
new B().me().doB();

所以,只有下面的代码是可以被调用的:

1
2
//legal
new B().me().doA();

然而,我们可以将其强制类型转换成B,如下代码:

1
2
//legal
((B) new B().me()).doB();

接下来,我们添加一个C类:

1
2
3
4
5
class C extends A{
public void doBad() {
System.out.println("Do C");
}
}

那么,下面的代码语句将通过静态类型检查:

1
2
//legal
((C) new B().me()).beBad();

编译器不知道它的真实类型,但是在运行期间将会抛出异常,因为B类型不能转换成C类型

Java静态绑定与动态绑定

https://blog.csdn.net/zhangjk1993/article/details/24066085

程序绑定的概念

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

静态绑定:

在程序执行前方法已经被绑定(也就是说在编译过程中就已经知道这个方法到底是哪个类中的方法),此时由 编译器或其它连接程序实现。例如:C。

针对java简单的可以理解为程序编译期的绑定;这里特别说明一点,java当中的方法只有final,static,private和构造方法是前期绑定

动态绑定、后期绑定: 在运行时根据具体对象的类型进行绑定

若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说, 编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。

动态绑定的过程:

  • 虚拟机提取对象的实际类型的方法表;

  • 虚拟机搜索方法签名;

  • 调用方法。

关于final,static,private和构造方法是前期绑定的理解

对于private的方法,首先一点它不能被继承,既然不能被继承那么就没办法通过它子类的对象来调用,而只能通过这个类自身的对象来调用。因此就可以说private方法和定义这个方法的类绑定在了一起。

final方法虽然可以被继承,但不能被重写(覆盖),虽然子类对象可以调用,但是调用的都是父类中所定义的那个final方法,(由此我们可以知道将方法声明为final类型,一是为了防止方法被覆盖,二是为了有效地关闭java中的动态绑定)。

构造方法也是不能被继承的(网上也有说子类无条件地继承父类的无参数构造函数作为自己的构造函数,不过个人认为这个说法不太恰当,因为我们知道子类是通过super()来调用父类的无参构造方法,来完成对父类的初始化, 而我们使用从父类继承过来的方法是不用这样做的,因此不应该说子类继承了父类的构造方法),因此编译时也可以知道这个构造方法到底是属于哪个类。

对于static方法,具体的原理我也说不太清。不过根据网上的资料和我自己做的实验可以得出结论:static方法可以被子类继承,但是不能被子类重写(覆盖),但是可以被子类隐藏。(这里意思是说如果父类里有一个static方法,它的子类里如果没有对应的方法,那么当子类对象调用这个方法时就会使用父类中的方法。而如果子类中定义了相同的方法,则会调用子类的中定义的方法。唯一的不同就是,当子类对象上转型为父类对象时,不论子类中有没有定义这个静态方法,该对象都会使用父类中的静态方法。因此这里说静态方法可以被隐藏而不能被覆盖。这与子类隐藏父类中的成员变量是一样的。隐藏和覆盖的区别在于,子类对象转换成父类对象后,能够访问父类被隐藏的变量和方法,而不能访问父类被覆盖的方法

由上面我们可以得出结论,如果一个方法不可被继承或者继承后不可被覆盖,那么这个方法就采用的静态绑定。

java的编译与运行

java的编译过程是将java源文件编译成字节码(jvm可执行代码,即.class文件)的过程,在这个过程中java是不与内存打交道的,在这个过程中编译器会进行语法的分析,如果语法不正确就会报错。

Java的运行过程是指jvm(java虚拟机)装载字节码文件并解释执行。在这个过程才是真正的创立内存布局,执行java程序。

java字节码的执行有两种方式: (1)即时编译方式:解释器先将字节编译成机器码,然后再执行该机器码;(2)解释执行方式:解释器通过每次解释并执行一小段代码来完成java字节码程序的所有操作。(这里我们可以看出java程序在执行过程中其实是进行了两次转换,先转成字节码再转换成机器码。这也正是java能一次编译,到处运行的原因。在不同的平台上装上对应的java虚拟机,就可以实现相同的字节码转换成不同平台上的机器码,从而在不同的平台上运行)

动态绑定的细节

前面已经说了对于java当中的方法而言,除了final,static,private和构造方法是前期绑定外,其他的方法全部为动态绑定。而动态绑定的典型发生在父类和子类的转换声明之下:比如:Parent p = new Children();

其具体过程细节如下:

  1. 编译器检查对象的声明类型和方法名

    假设我们调用 x.f(args) 方法,并且x已经被声明为C类的对象,那么编译器会列举出C 类中所有的名称为f 的方法和从C 类的超类继承过来的f 方法

尤其是对于Parent p = new Child();这种情况,p被声明为Child类的对象,会找到其所有祖先类的方法。

  1. 接下来编译器检查方法调用中提供的参数类型

    如果在所有名称为f 的方法中有一个参数类型和调用提供的参数类型最为匹配,那么就调用这个方法,这个过程叫做“重载解析”。参数类型匹配过程中以静态类型为准进行匹配

  2. 当程序运行并且使用动态绑定调用方法时,虚拟机必须调用同x所指向的对象的实际类型相匹配的方法版本。假设实际类型为D(C的子类),如果D类定义了f(String),那么该方法被调用,否则就在D的超类中搜寻方法f(String),依次类推。(后面习题一中有例子)

JAVA 虚拟机调用一个类方法时(静态方法),它会基于对象引用的类型(通常在编译时可知)来选择所调用的方法。相反,当虚拟机调用一个实例方法时,它会基于对象实际的类型(只能在运行时得知)来选择所调用的方法,这就是动态绑定,是多态的一种。动态绑定为解决实际的业务问题提供了很大的灵活性,是一种非常优美的机制。

与方法不同**,在处理java类中的成员变量(实例变量和类变量)时,并不是采用运行时绑定,而是一般意义上的静态绑定**。所以在向上转型的情况下,对象的方法可以找到子类,而对象的属性(成员变量)还是父类的属性(子类对父类成员变量的隐藏)。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Father {
protected String name = "父亲属性";
}
  

public class Son extends Father {
protected String name = "儿子属性";

public static void main(String[] args) {
Father sample = new Son();
System.out.println("调用的属性:" + sample.name);
}
}

结论,调用的成员为父亲的属性。

这个结果表明,子类的对象(由父类的引用handle)调用到的是父类的成员变量。 所以必须明确,运行时(动态)绑定针对的范畴只是对象的方法

现在试图调用子类的成员变量name,该怎么做?最简单的办法是 将该成员变量封装成方法getter形式。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Father {
protected String name = "父亲属性";

public String getName() {
return name;
}
}  

public class Son extends Father {
protected String name = "儿子属性";

public String getName() {
return name;
}

public static void main(String[] args) {
Father sample = new Son();
System.out.println("调用的属性:" + sample.getName());
}
}

结果:调用的是儿子的属性

java因为什么对属性要采取静态的绑定方法。这是因为静态绑定是有很多的好处,它可以让我们在编译期就发现程序中的错误,而不是在运行期。这样就可以提高程序的运行效率!而对方法采取动态绑定是为了实现多态,多态是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
public class  Bird{
public void fly(Bird p) {System.out.println(“Bird fly with Bird”);}
}
public class Eagle extends Bird {
public void fly(Bird p) {System.out.println(“Eagle fly with Bird!”);}
public void fly(Eagle e) { System.out.println(“Eagle fly with Eagle!”);}
}

Bird p1 = new Bird () ;
Bird p2 = new Eagle () ;
Eagle p3 = new Eagle () ;

p1.fly( p1 ) ;//BB
p1.fly( p2 ) ;//BB
p1.fly( p3 ) ;//BB
//以上三个静态类型都是Bird而且动态类型也是,所以只会调用到Bird类中的方法
p2.fly( p1 ) ;//EB
//这个是精准的方法参数匹配
p2.fly( p2 ) ;//EB
//p2动态类型是Eagle,但是编译器根据静态类型Bird作为参数类型
p2.fly( p3 ) ;//EB
//似乎结果应该是EE,但在编译时编译器只知道p2的静态类型是Bird,所以根据参数动态绑定了Bird中的方法,在运行时程序运行并且使用动态绑定调用方法时,虚拟机必须调用同p2所指向的对象的实际类型相匹配的方法版本,所以执行的是P2的fly(Bird)
p3.fly( p1 ) ;//EB
p3.fly( p2 ) ;//EB
p3.fly( p3 ) ;//EE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A {      
public String show(D obj)...{            
return ("A and D");   
}
public String show(A obj)...{  
return ("A and A");     
}
}

class B extends A{        
public String show(B obj)...{
return ("B and B");
}       
public String show(A obj)...{               
return ("B and A");       
}
}

class C extends B...{}
class D extends B...{}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
A a1 = new A();
A a2 = new B();
B b = new B();
C c = new C();
D d = new D();
System.out.println(a1.show(b));  
System.out.println(a1.show(c));  
System.out.println(a1.show(d));  
System.out.println(a2.show(b)); 
System.out.println(a2.show(c));        
System.out.println(a2.show(d));  
System.out.println(b.show(b));  
System.out.println(b.show(c));   
System.out.println(b.show(d));   
1
2
3
4
5
6
7
8
9
Void order (Dessert d, Cake c);
Void order (Pie p, Dessert d);
Void order (ApplePie a, Cake c);

order (aDessert, aCake);//执行方法一
order (anApplePie , aDessert);//执行方法二
order (aDessert , aDessert);//错误
order (anApplePie , aChocolateCake);//执行方法三
order (aPie , aCake);//错误

重载方法匹配算法

第一步,找精确匹配(形参实参精确匹配的同一类型)找到,则执行,找不到转第二步。

第二步,找可行匹配(符合替换原则的匹配,即实参所属类是形参所属类的子类),没找到可行匹配,报错;只找到一个可行匹配,执行可行匹配对应的方法;如果有多于一个的可行匹配,转第三步。

第三步,多个可行匹配两两比较,如果一个方法的各个形参,或者:与另一个方法对应位置形参所属类相同,或者:形参所属类是另一个方法对应位置形参所属类的子类,该方法淘汰另一个方法。

最后,如果只剩一个幸存者,执行;如果多于一个幸存者,报错。