Java学习笔记之基础篇
Java学习笔记之基础篇
目录
这是Java学习过程中的基础篇笔记整理,里面内容主要来自《Java核心技术》(卷1、卷2),一些笔试题,相关博客(具体已不可考,如有侵犯,望告知),以及自己的一些理解总结而成。
Java如何体现平台的无关性?
答:主要体现在字节码这个文件上,java源文件(.java)通过编译器生成字节码文件(.class),字节码文件(.class)通过Java虚拟机中的解释器翻译成特定机器上的机器码,然后在特定的机器上运行。它只跟虚拟机有关,所以只要平台装有JDK就可以实现一次编译多处运行,所以像在Java中每种数据类型被设计成固定范围大小,就是为了能够在不同机器上面得到同样的结果,而像C/C++这样编译产生的目标模块(.obj)与平台相关的可能因为处理器的位数不同得到不同的结果(16位处理器int占2个字节,32位占4字节)
面向对象(OO)的理解
就是万物皆对象,面向对象的关注点在于数据结构上面(面向过程编程则是关注于操作数据结构的算法),不关心对象的具体实现,只要能够满足用户需求即可,如一个工匠做一把椅子他首先关心的是椅子,然后是做椅子工具,它有三大特征:封装、继承、多态
- 封装
隐藏对象的属性和实现细节,只提供访问的公共接口,让类中的方法不能直接的访问其他类的实例域(指对象中的数据)。程序仅通过对象的方法与对象数据进行交互。 - 继承
将一类事物共有的属性和行为抽象成一个父类,子类在继承父类基础上进行扩展(所以会有自己特有的功能) - 多态
同一操作作用在不同的对象时会有不同的语义。没有继承,就没有多态,如果说继承耦合度高的话,那么多态就是为了解除父子类继承的耦合度,简单来说多态允许父类引用(或接口)指向子类(或实现类)对象。
注:
- 封装和继承是为了使代码重用,多态则是为了实现接口重用。
- 重载和重写体现了多态。
- 只有类中的方法才有多态,成员变量是没有多态的。
面向对象和面向过程编程的区别
- 面向过程编程
面向过程编程是一种以过程为中心的编程思想,分析出解决问题的步骤,然后用函数把这些步骤一步一步实现(过程不可重用,每次都需要重新设计)。数据和对数据的操作是分离的。 - 面向对象编程 (OOP)
面向对象编程是将一类事物抽象成一个对象,它不关心对象的具体实现,通过调用相应对象来解决相应问题(对象可以重用)。数据和对数据的操作是绑定在一起的。
举个例子:面向过程就是蛋炒饭,先炒蛋,在加饭炒,然后加油,然后加盐,最后一旦完成了,蛋和饭就是不可分离的,如果想要将蛋换成其他的材料,就需要重新炒一份;面向对象就是盖浇饭,饭和菜是分离的,我们将盖浇饭抽象成一个对象,这样就可以你想要什么菜最后就加什么菜,你想要青椒肉丝,就浇上青椒肉丝,变成青椒肉丝盖浇饭,想要鱼香肉丝,就在盖浇饭这个对象上浇上鱼香肉丝变成鱼香肉丝盖浇饭。由此可以看出,面向过程编程按步骤划分,耦合度高,维护性差;面向对象编程,先将一类事物抽象成对象,然后按功能划分,耦合度低,维护性强,可重用。
例题:编写一个驾驶汽车的方法
- 面向过程的程序设计: 直接编写一个驾驶汽车的方法,void driveCar();
- 面向对象的程序设计: 先将一辆汽车看成一个对象,把所有汽车对象的共性抽取出来,设计一个类Car,类中有一个方法void drive(),用Car这个类实例化一个具体的对象car,调用:car.drive()。
面向对象三大特征
- 封装
隐藏对象的属性和实现细节,只提供访问的公共接口,让类中的方法不能直接的访问其他类的实例域(指对象中的数据)。程序仅通过对象的方法与对象数据进行交互。 - 继承
- 在Java继承中,父类的构造函数不能被继承,而是显示或者隐式调用(默认调的是父类无参构造函数,如果想调用带参的需要显示调用super(XX))
- 在Java继承中,子类可以继承父类所有的成员变量,包括私有成员(final修饰的类不能被继承(final方法或常量可以继承),也就不存在父类这说法)
注:私有的也可以继承,只是由于修饰符的原因不能直接调用(利用反射可以调用,其实这也说明了私有被继承了,不然怎么反射可以得到这个私有成员呢),继承的final方法不允许被子类覆盖,变量不允许改变。如果同时出现继承和实现,则必须先继承(extends)再实现(implements)
说明:使用反射可以看出子类是继承了父类的私有方法的(不管是否是final),只是直接调用父类的私有方法是不可以的,不过利用反射的方式可以调用。字段同理。
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Method;
class Parent{
Parent() {
//this(2);//构造函数可以相互调用,但是需要放在第一排
System.out.println("调用父类构造方法!");
}
Parent(int i) {
System.out.println("调用有参父类构造方法!");
}
private static void staticParent() {
System.out.println("调用父类静态方法");
}
private final void finalParent() {
System.out.println("调用父类final方法");
}
private void printParent(){
System.out.println("调用父类私有方法");
}
}
class Child extends Parent {
public void printChild(){
System.out.println("调用子类公有方法");
}
}
public class Test {
public static void main(String[] args) throws Exception {
//获取子类
Class clazz = Class.forName("Child");
//得到父类
Class superClass = clazz.getSuperclass();
//得到父类非继承的所以方法
Method[] methods = superClass.getDeclaredMethods();
//设置私有方法可以被访问
AccessibleObject.setAccessible(methods,true);
for (Method m:methods) {
System.out.println();
System.out.println("子类调用方法"+m.getName()+"()的调用结果:" );
m.invoke(new Child());
}
}
}
/*运行结果:
子类调用方法finalParent()的调用结果:
调用父类构造方法!
调用父类final方法
子类调用方法printParent()的调用结果:
调用父类构造方法!
调用父类私有方法
子类调用方法staticParent()的调用结果:
调用父类构造方法!
调用父类静态方法
*/
注意:子类不可以继承父类的构造方法,只可以调用父类的构造方法。(因为构造函数是每个类用来初始化的)
Java用关键字extends代替了C++中的冒号(:)。在Java中所有继承都是公有继承,而没有C++中私有继承和保护继承。
- 多态
- 父类对象赋给子类引用
- 编译看左边,运行看右边
注:只有类中的方法才有多态的概念,成员变量是没有多态的(因为成员变量的值取父类还是子类不是由创建的对象决定,而是由所定义变量的类型,这是在编译时期就确定的)。
父类:Parent
父类成员变量 tempValue
父类方法 parentMethod()
子类:Son
子类成员变量 tempValue
子类方法 sonMethod()
Parent p = new Son();
编译时,编译器把p看成一个Parent对象,运行时会把p看成Son的对象。
那么p.parentMethod();//正确,因为编译时p为Parent对象
但是p.sonMethod();//错误,因为编译时p显示Parent对象特性,而sonMethod不是Parent的方法
除非父类也有个 sonMethod()方法,这样编译通过,运行时调用子类(不是父类) sonMethod()方法。
对于p.tempValue调用的是父类的那个,而不是子类,没有显示多态特性
注意:不能将超类的引用赋给子类变量,必须进行类型转换(可能会通过编译,但是会导致一些意想不到的事情,运行时会报错)
举个例子:
public class Base
{
private String baseName = "base";
public Base()
{
callName();//Base b = new Sub();调用的是子类的callName()方法,但是子类尚未构造,故输出的baseName 为 null
}
public void callName()
{
System. out. println(baseName);
}
static class Sub extends Base
{
private String baseName = "sub";
public void callName()
{
System. out. println (baseName) ;
}
}
public static void main(String[] args)
{
Base b = new Sub();
}
}
//最后输出结果为null
分析:子类继承父类,在实例化时会先去实例化父类。所以new Sub();在创造派生类的过程中首先去创建基类对象,然后才能创建派生类。
类的加载顺序:
(1) 父类静态代码块(包括静态初始化块,静态属性,但不包括静态方法)
(2) 子类静态代码块(包括静态初始化块,静态属性,但不包括静态方法 )
(3) 父类非静态代码块( 包括非静态初始化块,非静态属性 )
(4) 父类构造函数
(5) 子类非静态代码块 ( 包括非静态初始化块,非静态属性 )
(6) 子类构造函数
创建基类即默认调用Base()方法,在方法中调用callName()方法,由于派生类中存在此方法,运行时显示子类特性,被调用的callName()方法是派生类中的方法,但是此时派生类还未构造,所以变量baseName的值为null,如果将Base中的callName()方法改为其它不一样的名字(比如callName2())那么由于派生类中没有此方法(也就是没被重写)就会调用自己的callName2()方法从而输出“Base”
注意:Base b = new Sub();它为多态的一种表现形式(父类对象赋给子类引用),声明是Base,实现是Sub类, **理解为 b 编译时表现为Base类特性,运行时表现为Sub类特性。**当子类覆盖了父类的方法后(意思是父类的方法已经被重写),题中 父类初始化调用的方法为子类实现的方法(子类实现的方法中调用的baseName是子类中的私有属性)。
再举个栗子
class C {
C() {
System.out.print("C");
}
}
class A {
C c = new C();
A() {
this("A");
System.out.print("A");
}
A(String s) {
System.out.print(s);
}
}
class Test extends A {
Test() {
super("B");
System.out.print("B");
}
public static void main(String[] args) {
new Test();
}
}
//输出结果为CBB
/*
分析:按照初始化过程,先初始化静态变量和静态代码块,都没有,接着初始化父类非静态变量和非静态代码块,所以C c = new C();执行 此时类C构造函数初始化,输出C,接着初始化父类构造函数,
这里就有一个问题:有两个该初始化哪一个或者说是否是都初始化,这关键看父类构造函数是否是显示调用,Test类中super("B");已经表示了是显示调用说明调的是带参那个,所以类A输出B,
最后是子类的非静态变量和非静态代码块,没有,就接着初始化构造函数,输出B
*/
注意:不能用子类声明父类实例化对象!(即不能Son son = new Parent(),因为子类的句柄指向父类空间时,有可能有部分子类的属性方法是父类所没有的,因此,子类句柄无法指向有效的空间,编译无法通过。)
例题:
public class Test {
public static void main(String[] args) {
System.out.println(new B().getValue());
}
static class A {
protected int value;
public A (int v) {
setValue(v);
}
public void setValue(int value) {
this.value= value;
}
public int getValue() {
try {
value ++;
return value;
} finally {
this.setValue(value);
System.out.println(value);
}
}
}
static class B extends A {
public B () {
super(5);
setValue(getValue()- 3);
}
public void setValue(int value) {
super.setValue(2 * value);
}
}
}
//输出:22 34 17,不是6 7 7
分析过程:
整个过程可分成三个部分:new B(),new B().getValue(),System.out.println(new B().getValue());
第一部分:new B()
首先需要明确一点:B继承自A类,所以实例化B需要先实例化A,但是父类A自己定义了构造函数,JVM就不再为它创建默认的构造函数了,所以需要子类B显示调用,
如果子类B中的构造函数里没有显示调用的话(即没有写super)会报错
由 super(5); 可知显示调用父类A的带参构造函数(),即
public A (int v) {//传入的v=5
setValue(v);
}
这里需要注意 setValue(v);调用的是子类B中的setValue方法(因为此时B类重写了setValue,父类方法被隐藏,所以没有显示调用的话就认为是子类重写的那个方法),故而转去执行子类B的
public void setValue(int value) {//传入的value=5
super.setValue(2 * value);
}
由于里面显示调用了父类的setValue方法,所以又转去执行父类的setValue方法,即
public void setValue(int value) {//传入的value = 2 * 5 = 10
this.value= value;
}
至此,父类的构造函数初始化完毕(也就是super(5);执行完毕)。
接着执行 setValue(getValue()- 3);这一个语句又可分为两个部分,一个是 getValue(); 一个是 setValue()
首先是 getValue() 方法,因为没有被子类覆盖所以调的是父类那个 getValue(),即
public int getValue() {//此时的value=10
try {
value ++;
return value;
} finally {
this.setValue(value);
System.out.println(value);
}
}
对于 try-catch-finally 语句块需要注意的是finally是一定会执行的,并且先于return之前执行,也就是说先执行 value++,然后就是finally块中语句,最后才返回value值
value++ 将value值变为11。此时11被暂时保存起来,等执行完finally后返回出去。
然后执行this.setValue(value); this指的是当前对象,而当前对象是子类B(注意不是父类A,因为是在子类B的构造函数中的),所以这里调的是子类的setValue方法(其实子类已经重写该方法,除非利用super显示调用,不然还是子类的setValue)
public void setValue(int value) {//传入的value=10 + 1 = 11
super.setValue(2 * value);
}
由于里面显示调用了父类的setValue方法,所以又转去执行父类的setValue方法,即
public void setValue(int value) {//传入的value = 2 * 11 = 22
this.value= value;
}
最后输出System.out.println(value);为22,所以第一个输出为22
接着就可以返回try里面的value值了,也就是11(注意不是返回22,虽然此时的value值是22,但是前面的value=11的值被暂时保存起来作为返回值,可以认为是另一个变量tempValue = 11作为返回值)
至此 getValue()执行完毕,返回11
然后 setValue(getValue()- 3); 就表示 setValue(11- 3);,即 setValue(8);
public void setValue(int value) {//传入的value=8
super.setValue(2 * value);
}
由于里面显示调用了父类的setValue方法,所以又转去执行父类的setValue方法,即
public void setValue(int value) {//传入的value = 2 * 8 = 16
this.value= value;
}
至此B的构造函数也初始化完毕(这是的AB都已经初始化完了,此时value为16),即 New B()结束
第二部分:new B().getValue()
public int getValue() {//此时的value=16
try {
value ++;
return value;
} finally {
this.setValue(value);
System.out.println(value);
}
}
首先执行value++; value=16+1=17;暂时存起来(可认为是存在tempValue里面,然后执行完finally块后返回),
接着this.setValue(value);即
public void setValue(int value) {//传入的value=17
super.setValue(2 * value);
}
由于里面显示调用了父类的setValue方法,所以又转去执行父类的setValue方法,即
public void setValue(int value) {//传入的value = 2 * 17 = 34
this.value= value;
}
最后输出System.out.println(value);为34,所以此时第二个输出为34,然后return tempValue的值,即返回17(注意不是34)
至此第二部分执行完毕,new B().getValue()返回了17
第三部分:System.out.println(new B().getValue());
new B().getValue()返回了17,也就是说System.out.println(new B().getValue());可换成System.out.println(17);所以第三个输出结果为17,
至此程序执行完毕,打印输出三个结果为22 34 17
静态绑定和动态绑定(后期绑定)
- 静态绑定:针对于private方法、static方法、final方法或者构造器
- 动态绑定:除静态绑定的那些情况之外的方法(如public方法,protected方法)
说明:超类的私有方法不可访问;静态方法(也叫类方法)是属于类的而不是对象的可以被继承,final方法子类能继承但不能对final方法进行重写(覆盖),但是可以被隐藏;构造器不可被继承,所以编译器可以明确知道该调用那个方法(也就是在编译时期就可以确定),这些在类被加载时就已经知道,不需对象的创建就能访问的,就是静态绑定的内容,所以叫静态绑定;除了上面情况之外,需要等对象创建出来,使用时根据堆中的实例对象的类型才进行取用的就是动态绑定的内容。在运行时由虚拟机根据具体对象去找事先为该对象的类所创建的方法表(里面列出了所有方法的签名和实际调用的方法)来决定调用哪个方法
延伸:类之间的关系
依赖(“uses-a”):如果一个类A的方法操纵(就是用到了)另一个类B的对象,我们就说A依赖类B(如果类B变化可能会影响到类A,所以应该尽可能将相互依赖的类减至最少,即让类之间的耦合度最小)
聚合(“has-a”,或称组合):表示整体和部分的关系。类A对象包含类B的对象(如果类A消失,类B依然可以继续使用),比如汽车和轮胎,汽车和发动机
继承(“is-a”):表示特殊与一般关系。比如交通工具和汽车,交通工具和飞机
组合(聚合)和继承的区别
组合是整体和部分的关系(在新类中创建已有类对象,重复利用已有类的功能)。继承是特殊和一般的关系(在原有类的基础上扩展新类的功能)。组合就是汽车和轮胎的关系;继承就是交通工具和汽车的关系。两者都可以实现代码的重用。继承耦合度比组合要高(因为组合关系中,部分与部分之间互不影响,但是继承关系中,子类依赖父类,当父类改变时,子类也要变)。
注意:除非两个类是特殊和一般的关系(即has-a),否则不要轻易使用继承,并且Java中能使用组合的就不要使用继承(组合重用原则)
重载(Overload)和重写(Override,也称覆盖)
多态的两种表现方式
- 重载(编译时多态)
- 必须具有不同的参数列表;
- 可以有不同的返回类型,只要参数列表不同就可以;
- 可以有不同的访问修饰符;
- 可以抛出不同的异常;
- 方法能够在一个类中或者在一个子类中被重载。
说明:方法重载要求方法具备不同的特征签名(参数个数和类型),返回值不包含在方法的特征签名中,所以返回值不参与重载的选择,但是在Class文件格式中,只要描述符不是完全一样的两个方法就可以共存(这样的话就不是重载了,要注意区分)。也就是说,两个方法如果有相同的名称和特征签名,凡是返回值不同,那他们也是可以合法的共存于一个Class文件中的(这里也是分情况的,不然会编译报错,所以注意理解,并且注意不是重载!!!)。
注意:
1)Java允许重载任何方法,其中main方法也可以被重载
2)对于继承来说,如果基类的方法时private,那么派生类是不能对它重载(因为子类虽然能够继承私有成员,但是由于是private,故不能直接访问,也就不能重载),子类中同名不同参的是一个新方法,不叫重载,如果子类中存在同名同参的方法,那说明子类覆盖了父类方法。
3)函数是不能以返回值来区分的
class Super{
public int f(){
return 1;
}
}
public class Sub extends Super{
public float f(){
return 2f;
}
public static void main(String[] args){
Super s = new Sub();
System.out.println(s.f());
}
}
//编译出错,函数不能用返回值来区分(因为编译器无法区分两个相同的函数)
重载就是一句话:同名不同参,返回值无关。
- 重写(运行时多态)
方法的重写(override)两同两小一大原则:
- 两同:方法名相同,参数类型相同
- 两小:子类返回类型小于等于父类方法返回类型(如果返回值是基本数据类型,那么重写方法和被重写方法必须一致。),子类抛出异常小于等于父类方法抛出异常
- 一大:子类访问权限大于等于父类方法访问权限。(如果一个方法不能被继承,则不能重写它。最典型的例子为,被覆盖的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行覆盖。静态方法不存在重写,形式上的重写只能说是隐藏)
注意:
1)对于重写而言,只有可见的实例方法才可以被重写。静态方法和私有方法都是隐藏,不是重写!!!
2)由于构造函数不能被继承,所以不能被重写(但可以重载)
3)父类中被final修饰的方法不能被重写
覆盖/重写:同名同参(两同两小一大)
说明:
重载是同一类中方法之间的关系(水平关系);重写是子类与父类之间的关系(垂直关系)
1)重载,多态在编译期的表现形式(静态绑定,编译时就可以确定调用那个方法)。出现在一个类中,判定重载的条件只有方法名一致,方法形参列表不同。其他的(如返回值,访问修饰符)都不可以判定。
2)重写,多态在运行期的表现形式(动态绑定,运行时才可以确定调用那个方法)。出现在子父类,实现类与接口中。判定重载有一个规律:“两同两小一大”
3)《深入理解JVM虚拟机》,重载就是在编译期根据方法形参的静态类型确定方法版本,重写就是在运行期根据实际调用者的实际类型确定方法版本(这体现了多态)。
抽象类和接口
- 两者相同点是都不能被实例化,只有实现了它们方法的子类才可以。
- 接口可以继承接口,抽象类可以实现接口(因为抽象类中可以有抽象方法和普通方法)
- 接口强调特定功能的实现(设计理念是“has-a”),抽象类强调所属关系(设计理念是“is-a”),这也是为什么提倡接口编程。
说明:一般而言,抽象类多用于同类事物中有无法具体描述的方法的场景,所以当子类和父类之间存在逻辑上的层次结构时,推荐使用抽象类,如红鸭子会游泳,黄鸭子也会有用,那么抽象出鸭子抽象类,里面有个游泳方法(和其它鸭子共有的方法);接口多用于不同类之间,定义不同类之间的通信规则,所以希望支持差别比较大的两个或者更多对象之间的特定交互行为时,推荐使用接口,如鸭子会游泳,狗也会游泳,如果让狗继承鸭子抽象类,那么就会继承鸭子的抽象方法,但是这些我们不需要,所以可以利用接口来实现两者特定交互行为
抽象类
1.抽象类中可以构造方法(因为抽象类中可以有一些变量,这些变量需要通过构造方法来初始化)
2.抽象类中可以存在普通属性,方法,静态属性和静态方法(所以抽象类可以有构造函数去初始化它们),抽象方法不能有方法体。
3.如果一个类中有一个抽象方法,那么当前类一定是抽象类;反之抽象类中不一定有抽象方法。
4.抽象类中的抽象方法,需要有子类实现,如果子类不实现,则子类也需要定义为抽象的(注意在实现时必须包含相同的或者更低的访问级别:public -> protected -> private)。
接口(它不是类,而是对类的一组需求描述,是抽象方法定义的集合)
1.接口中没有构造方法,也不能实例化接口的对象。
2.在接口中只有常量没有变量,因为定义的变量,在编译的时候都会默认加上public static final(所以必须赋初值)
3.接口中方法的默认修饰符时public abstract(意味着接口中的方法都是抽象方法且是public的,在接口中声明时可以省略这两个关键字,但是实现类中必须带public修饰符)。
4.在接口中只有方法的声明,没有方法体,并且抽象方法也是没有方法体的(也就是没有大括号{}),JDK8中,接口中的方法可以被default和static修饰,但是!!!被修饰的方法必须有方法体
5.接口可以实现多继承功能(因为Java中只能单继承)
6.接口中定义的方法都需要有实现类来实现,如果实现类不能实现接口中的所有方法则实现类定义为抽象类。
注:
-
一个抽象类可以是public、protected、default(不能用private修饰), 接口只有public(没写就默认为public,所以不能被private和protected修饰);
-
一个抽象类中的可以存在普通属性和方法,方法可以是public、protected、default,接口中属性只能为public static final,方法只能为public abstract。
-
类对接口的实现(实现接口时是需要加上public的),其实体现了多态性,因为类需要重写接口中所有的抽象方法。而重写需要满足两同两小一大:
-
接口与类的真正区别在于当一个类在初始化时,要求父类全部都已经初始化了,但是一个接口在初始化时,并不要求父接口全部都完成了初始化,只有在真正使用到父接口时(如引用接口中定义的常量)才会初始化
延伸:不同版本间的抽象类和接口的访问权限
关于抽象类
JDK 1.8以前,抽象类的方法默认访问权限为protected
JDK 1.8时,抽象类的方法默认访问权限变为default
关于接口
JDK 1.8以前,接口中的方法必须是public的
JDK 1.8时,接口中的方法可以是public的,也可以是default的
JDK 1.9时,接口中的方法可以是private的
问:java中,一个类实现某个接口,必须重写接口中的所有方法吗?
答:不一定,关键要看子类是否是抽象类。
如果子类不是抽象类,则必须实现接口中的所有方法;
如果子类是抽象类,则可以不实现接口中的所有方法,因为抽象类中允许有抽象方法的存在!
注意:Java中可能存在有些接口没有任何方法,这些没有任何方法声明的接口叫标识接口,它仅仅充当一个标识的作用
//矿石
interface Ore{}
class Glod implements Ore{//金矿
public String toString(){
return "This is Gold";
}
}
class Copper implements Ore{//铜矿
public String toString(){
return "This is Copper";
}
}
未完待续…
上一篇: Java内部类
下一篇: 5.Java之字符串