java 学习笔记一
重载函数
函数名称必须相同,函数的参数类型/个数必须有所不同
不能以返回值类型区分
public static int add(int a, int b) { }
public static int add(double a, double b) { } // 可与第一行函数重载
public static double add(int a, int b) { } // 不可重载
可变参数
概念:java允许将同一个类中多个同名且功能相同但参数个数不同的方法,封装成一个方法。
基本语法:
- 访问修饰符 返回类型 方法名(数据类型… 形参名)
例子:
// 使用方法重载
// public int sum(int a, int b) {
// return a + b;
// }
// public int sum(int a, int b, int c) {
// return a + b + c;
// }
// public int sum(int a, int b, int c, int d) {
// return a + b + c + d;
// }
// 使用可变参数优化
// ... 表示 可变参数,这里的 nums 可视为数组
public static int sum(int... nums) {
System.out.println("输入参数的个数:" + nums.length);
int sum = 0;
for (int num : nums) {
sum += num;
}
return sum;
}
public static void main(String[] args) {
System.out.println(sum(1,465,789,123,46,564));
}
注意事项:
- 可变参数的实参可以是 0 或者任意多个
- 可变参数的实参可以直接是数组,像调用刚刚那个方法
System.out.println( new int[]{1,465,789,123,46,564});
,这样也是没问题的 - 可变参数的实参实际上就是数组
- 可变参数可以和普通类型参数一起放在形参列表,但是必须保证可变参数在最后
- 一个形参列表只能出现一个可变参数
对象与基本类型:
新建一个对象相当于一个C语言中的指针,称为Reference
对象赋值是Reference赋值
而基本类型赋值是值拷贝
new出一个对象的默认值:
产生一个新的对象后,如果没有赋值,会有一个内部默认属性值:
类型 | 默认值 |
---|---|
short | 0 |
int | 0 |
long | 0 |
boolean | false |
char | ‘\u0000’ |
byte | 0 |
float | 0.0f |
double | 0.0d |
构造函数:
构造函数名称必须和类名一样,且没有返回值
每个Java类必须要有构造函数,否则编译器自动添加一个空的构造函数
可以有多个构造函数,只要形参列表不相同即可
构造函数只能通过new自动调用
构造函数内可以调用其他的成员方法,包括static关键字修饰的静态方法
信息隐藏:
类的成员属性是私有的 private
类的方法是共有的,通过方法修改属性值 public
this指针:
this指针负责指向:
- 本类中的成员变量
- 本类中的成员方法
也可以替代本类中的构造函数
super() 构造器
super() 是父类的构造函数。
- 对于子类来说,如果构造函数的第一句不是 super(),编译器会自动增加一句 super()。如果构造函数第一句是程序员自己写的 super,那么不会自动添加。
- 不只是限于无参的父类构造函数,有参数的 super 也可以。
- 若父类没有无参的构造函数,那么子类需要通过 super 指定使用父类的哪个构造函数。
- this() 也只允许放在构造函数的第一行,因而其与 super() 在一个构造函数中只能存在一个。
- 一个构造函数中只能有一个super
- 父类构造器的调用并不是只有直接父类,而会一直往上回溯的基类 Object
如
public class A{
public A(){
System.out.println("dadadadadadad");
}
public A(int a){
System.out.println("I am dadadadadadad");
}
}
public class B extends A{
public B(){
// 编译器自动加super()
System.out.println("son son son son");
}
public B(int a){
// 编译器自动加super()
super(6666);
System.out.println("I am son son son son");
}
public static void main(String[] a){
B obj1 = new B();
System.out.println(">>>>>>>>>>>>>>>>>>>");
B obj2 = new B(10);
}
}
输出为:
dadadadadadad
son son son son
>>>>>>>>>>>>>>>>>>>
I am dadadadadadad
I am son son son son
如果class B定义修改为:
public class B extends A{
public B(){
// 编译器自动加super()
System.out.println("son son son son");
}
public B(int a){
super(a)
System.out.println("I am son son son son");
}
public static void main(String[] a){
B obj1 = new B;
System.out.println(">>>>>>>>>>>>>>>>>>>");
B obj2 = new B(10);
}
}
那么输出变为:
dadadadadadad
son son son son
>>>>>>>>>>>>>>>>>>>
I am dadadadadadad
I am son son son son
继承
继承使用关键字 extends
class B extends A {}
上述例子即代表,类B 继承了 类A。那么 B 就称作子类或者派生类,A 就称作父类或者超类。
- 子类拥有父类非私有(private)的所有的属性和方法
- Java 是单继承机制,即一个类最多直接继承一个父类
- 不能滥用继承,子类和父类应该满足 is-a 原则。例如
Cat extens Animal
,显然 Cat is a Animal - Object 是顶层父类,所有类都是它的派生类
- 子类必须调用父类的构造器(显示或者隐式),完成父类的初始化。
- 当创建子类对象时,无论使用子类的哪个构造器,都会默认调用父类的无参构造器,除非子类构造器中显示的调用了父类的某一个构造器,或者是使用了 this() 指向了本类的另一个构造器
super 关键字
super 代表父类的引用,用于访问父类的属性、方法
- 访问父类的属性,但不能访问父类的私有属性
- 访问父类的方法,但不能访问父类的私有方法
- 访问父类的构造器
- 当子类的方法、属性和父类的重名时,需要使用 super 来指代父类的成员;如果没有重名,那么使用 super、this、直接访问效果都是一样的
- super 的访问不限于直接父类,若爷爷类有和本类同名的成员,也可以访问爷爷类的成员。若多级父类都有该成员,则逐级查找,使用第一个也就是和本类继承关系最近的成员。
super 和 this 的比较
区别点 | this | super |
---|---|---|
访问属性 | 访问本类属性,本类没有则向上回溯从父类中查找 | 从父类开始查找 |
调用方法 | 访问本类属性,本类没有则向上回溯从父类中查找 | 从父类开始查找 |
调用构造器 | 调用本类构造器,必须在构造器首行 | 调用父类构造器,必须在构造器首行 |
特殊 | 表示当前对象 | 子类中访问父类对象 |
方法重写
是指子类有一个方法,与父类的某个方法的名称、返回类型、参数都一样,这样就称子类重写(覆盖)了父类这个方法。
注意:
- 子类的方法名、参数要和父类的完全一样
- 子类的返回类型要么和父类的一样,要么是父类返回类型的子类。比如父类返回类型为 Object,子类可以是 Object,也可以是 String
- 子类方法不能缩小父类方法的访问权限。如父类本身是 protected,子类可以是 public,但不可以是 private
方法重载与重写的比较
名称 | 发生范围 | 方法名 | 形参列表 | 返回类型 | 修饰符 |
---|---|---|---|---|---|
重载 | 本类 | 一致 | 不一致 | 无要求,都可 | 无要求 |
重写 | 子类与父类 | 一致 | 一致 | 要么一致,要么子类返回类型是父类的返回类型的子类 | 不可缩小访问权限 |
抽象类
只要用关键字abstract声明的类就是抽象类。
抽象类可以理解成由于父类方法的不确定性导致的,在当父类的某些方法需要声明,但是不确定如何实现时,可以将其声明为抽象方法,这个类就是抽象类
类: 属性(0或多个)+ 方法(0或多个)
完整的类: 所有的方法都有实现(方法体)
类可以没有方法,但是有方法就要实验,这样的类才是完整类,完整的类才可以实例化:被new出来
- 抽象类指用关键字abstract声明的类
- 抽象类必须使用关键字abstract声明,事实上只要用 abstract 修饰,抽象类中可以没有 abstract 方法
- 抽象类的组成:成员变量、具体方法、抽象方法(使用关键字abstract声明,不一定有)。不过抽象类一般指含有抽象方法的类
- 抽象方法不能有主体,即大括号
- 抽象方法不能使用除 public 之外的修饰符修饰,这样才可以保证方法可以正常重写
- 抽象类中也可以有静态成员和构造方法
子类可以继承抽象类,但是一定要实现父类所有的抽象方法。否则,子类也要定义为抽象类
接口
如果类的所有方法都未实现,这个类可被认为是接口interface,接口中,抽象方法可以省略关键字 abstract
不过,JDK 8 及之后的版本中,接口内也可以有实现的方法和静态方法(需要加关键字 default))。
辨别类和接口的主要区别就在于关键字,取决于声明的时候是 class 还是 interface
如:
public interface People { // 一个接口
public void eat();
public void sleep();
// jdk 8 以后可以有实现方法,但是需要加关键字 default
default public void something() {
System.out.println("do something");
}
// jdk 8 以后可以有静态方法,但是需要加关键字 default
default static void anything() {
System.out.println("do anything");
}
}
public interface Climb { // 一个接口
public void climbTree();
}
// 继承了两个其它接口的接口
public interface Adult extends People, Climb {
}
所以,接口的构成为:
interface 接口名 {
// 属性
// 方法:1. 抽象方法 2. 默认的实现方法,加 default 关键字 3. 静态方法,加 default 关键字
}
接口可以继承多个接口,没有实现的方法会叠加。
类只可以继承(extends)一个类,但是可以实现(implements)多个接口。继承和实现可以同时出现。
例如:类C继承于类A,同时实现接口B,那么C的方法只会在A或者C中实现。
接口可以继承(多个)接口,没有实现的方法将会叠加。
接口中可以定义变量,但一般是常量。
类实现(不是继承)接口,必须实现接口的所有方法。否则,类要定义为抽象类
public class Man implements People {
public void eat(){
System.out.println("I can eat");
}
public void sleep(){
System.out.printlb("I can sleep");
}
}
// 有方法未实现,定义为抽象类
public abstract class Baby implements People {
// 抽象方法
public abstract void eat();
public void sleep() {
System.out.printlb("I can sleep");
}
}
// 继承加实现,注意extends需放在implements前面
public class Child extends Baby implements Climb {
public void eat() {
System.out.println("I can climb eat");
}
public void climbTree() {
System.out.println("I can climb tree");
}
}
public class Man implements Adult {
public void eat() {
System.out.println("I can climb eat");
}
public void sleep() {
System.out.printlb("I can sleep");
}
public void climbTree() {
System.out.println("I can climb tree");
}
}
接口使用细节
- 接口不能被实例化(接口类型变量可以指向实现了接口的对象实例)
- 接口中所有的方法是 public 方法(即使不声明修饰符,也会默认是 public),接口中的抽象方法可以不用 abstract 修饰
- 一个普通类实现接口,必须实现接口的所有方法
- 抽象类实现接口,可以不实现接口的方法
- 一个类可以同时实现多个接口
- 接口中的属性只能是 final 的,而且是 public static final 修饰符(即使不声明修饰符,也会默认为这个),也就是一个常量
- 接口中属性的访问方式:接口名.属性名
- 一个接口不可以继承其他的类,但是可以继承多个别的接口
- 接口的修饰符和类的修饰符一样,只能是 public 或者默认的
接口和继承的对比
当子类继承了父类,就会拥有父类的非私有的所有功能
而子类若需要扩展功能,就可以通过实现接口的方式进行扩展
实现接口就像是对Java单继承机制的一种补充
接口和继承解决的问题不同
- 继承的价值:解决代码的复用性和可维护性
- 接口的价值:在于设计,设计好各种规范,让其它类实现具体功能
接口比继承更加灵活
- 接口比继承灵活性更高,继承满足 is - a 的关系,而接口只需满足 like - a 的关系
接口在一定程度上实现了代码解耦,即接口的规范性 + 动态绑定机制
接口的多态传递
假设现在有:接口 IA 和 接口 IB,其中 IB 继承了 IA,如果有一个类 CA 继承了 IB。那么可以有
IB ib = new CA(); // 这个无需多说
IA ia = new CA(); // 因为 IB 继承了 IA,所以这个也可,这就叫多态传递,类似于向下转型
封装
封装(encapsulation)就是把抽象出来的数据(属性)和对数据的操作(方法)封装在一起,数据被保护在内部,程序只能通过规定的操作(方法)才可以对数据进行操作。
封装的好处:
- 隐藏方法的实现细节
- 保护并验证数据
实现方法:
- 属性私有化 private 修饰属性
- 设置公共(public)的 get() 和 set() 方法,只能通过这两个方法获取属性。
- 在set()方法中,可以先加入数据的验证,保证合理再设置
- 在get()方法中,可以先加入权限的验证,用于保护数据
类转型
变量之间支持相互转化。
类之间可以相互转型,但是只限于有继承关系的类。
- 子类可以转换为父类(从大变小,向上转型),父类不可以转换为子类(从小变大,向下转型,不允许)
- 子类拥有父类所有的财产,子类可以变成父类
- 转型后父类不可以访问子类特有的成员
Human obj1 = new Man(); // Man extends Human, 这是对的
Man obj2 = new Human(); // Man is a derived class of Human, 这是错的
有一种情况例外:父类本身就是子类转化过来的。
Human obj1 = new Man(); // Man extends Human, 这是对的
Man obj2 = (Man) obj1; // obj1 本身就是Man, 这是对的
子类继承父类的所有方法,但是子类可以重写(复写、覆盖,但不是重载)父类的方法,即重新定义一个名字、参数和父类一样的方法。
子类方法的优先级高于父类的。
子类转为父类后,调用共有的方法,依旧是执行子类中实现的方法。
例子:
public class Human {
public void eat() {
System.out.println("I can eat!");
}
}
public class Man extends Human {
public void eat() {
System.out.println("I can eat more!");
}
public void plough() {
System.out.println("I can plough!");
}
public static void main(String[] a) {
Man obj1 = new Man();
obj1.eat();
Human obj2 = (Human)obj1;
obj2.eat();
Man obj3 = (Man)obj2;
obj3.eat();
}
}
输出结果是:
I can eat more!
I can eat more!
I can eat more!
可见三个对象的eat()方法都是子类Man中定义的那个。
obj2的eat()方法也为子类中定义的。因为子类中定义的方法优先级高于父类。且obj2是由obj1类转型过去的,实际上用的还是obj1的那块内存,不过属于子类的那部分方法在obj2中被隐藏了。
而obj3重新类转型为子类,所用的内存与obj1仍然相同,且无隐藏内容。
当使用判断语句时,会发现这三个对象实际上是相等的。
类型转换带来的作用就是多态。
多态
多态基本介绍
- 方法和对象具有多种形态。其建立在封装和继承基础之上。
重载体现多态
- 在一个类中使用重载实现多个同名方法,在调用时根据输入参数的不同而有不同的效果,即是多态的一种体现。
重写体现多态
- 子类重写父类的方法,那么对于子类对象和父类对象,虽然都有一个方法名和参数相同的方法,却会有不同的效果
多态的作用:
- 以统一的接口来操纵某一类中不同的对象的动态行为,即每个对象的行为由自己决定
- 对象之间的解耦
例子:
首先定义一个接口和两个类
public interface Animal {
public void eat();
public void move();
}
public class Cat implements Animal {
public void eat() {
System.out.println("Cat can eat!");
}
public void move() {
System.out.println("Cat can move!");
}
}
public class Dog implements Animal {
public void eat() {
System.out.println("Dog can eat!");
}
public void move() {
System.out.println("Dog can move!");
}
}
然后写一个测试类
public class AnimalTest {
public static void main(String[] args) {
Animal[] as = new Animal[4];
as[0] = new Cat();
as[1] = new Dog();
as[2] = new Cat();
as[3] = new Dog();
for (int i = 0; i < as.length; i++) {
as[i].move(); // 调用每个元素自身的move方法
}
for (int i = 0; i < as.length; i++) {
haveLunch(as[i]);
}
}
public static void haveLunch(Animal a) {
a.eat();
}
}
输出结果是:
Cat can move!
Dog can move!
Cat can move!
Dog can move!
Cat can eat!
Dog can eat!
Cat can eat!
Dog can eat!
可见方法会因为类的不同而不同。
对象的多态
- 对象的编译类型是定义对象时就确定的,不可改变
- 对象的运行类型是可变的(通过类型转换)
- 定义对象时,其 = 左边是编译类型,右边是运行类型
- 对象的编译类型和运行类型可能不一致
例子:
Animal animal = new Dog(); // animal 的编译类型是 Animal,运行类型是 Dog
animal = new Cat(); // animal 的运行类型更改为 Cat
特别需要注意,属性没有重写之说
- 属性的值看编译类型
instanceOf 比较方法,用于判断对象的运行类型是否为XX类型或XX类型的子类型。
这样就可以使用该方法来做运行类型未知的对象的向下转型。
如:
// 假设有父类 Person,子类 Man
Person[] p = new Person[5];
...
p[2] = new Man(); // 即数组 p 的第三个元素是 Man 对象
...
// 就可以在遍历的时候使用 instanceOf 方法
// 在当前元素运行类型是 Man 时,使用 Man 独有的方法,或者做一些特别的操作
for (int i = 0; i < 5; i++) {
if (p[i] instanceOf Man) {
// do something
}
...
}
动态绑定机制
Java 的动态绑定机制是指:
- 当调用对象方法的时候, 该方法会和该对象的内存地址/运行类型相绑定
- 当调用对象属性的时候,没有动态绑定机制,使用的是编译类型对应的属性(哪里声明,使用哪里)
接下来通过三个来回的例子理解一下:
// 先定义两个类
public class A {
public int i = 10;
public int sum() {
return getI() + 10;
}
public int sum1() {
return i+10;
}
public int getI() {
return i;
}
}
public class B extends A{
public int i = 20;
@Override
public int sum() {
return i + 20;
}
@Override
public int getI() {
return i;
}
@Override
public int sum1() {
return i + 10;
}
}
// 然用写一个测试类
public class Test {
public static void main(String[] args) {
A a = new B(); // 向上转型
// a 的运行类型是 B
// 接下来调用的方法是 B类 中的方法
// 那么运行结果显而易见
System.out.println(a.sum()); // 40
System.out.println(a.sum1()); // 30
}
}
紧接着上面的例子,如果 将子类 B 中的 sum() 方法注释掉,再运行会有不一样的结果。如下:
public class A {
public int i = 10;
public int sum() {
// B类 调用该方法,但是该方法使用了 getI() 方法,
// B类 中也有 getI(),于是触发动态绑定机制,调用 B类 的该方法
return getI() + 10;
}
public int sum1() {
return i+10;
}
public int getI() {
return i;
}
}
public class B extends A{
public int i = 20;
@Override
// 注释掉该方法
// public int sum() {
// return i + 20;
// }
@Override
public int getI() { // 该方法返回本类的属性 i ,即 20
return i;
}
@Override
public int sum1() {
return i + 10;
}
}
// 然用写一个测试类
public class Test {
public static void main(String[] args) {
A a = new B(); // 向上转型
// a 的运行类型是 B
// B类 的sum()方法已经注释,所以这里调用的是 A类 的sum()方法
// 在sum()中又调用了共有的getI()方法,触发动态绑定,使用B类的getI()
System.out.println(a.sum()); // 30
// sum1()方法不变
System.out.println(a.sum1()); // 30
}
}
那么,若再将 B类 的 sum1() 方法注释,如下:
public class A {
public int i = 10;
public int sum() {
// B类 调用该方法,但是该方法使用了 getI() 方法,
// B类 中也有 getI(),于是触发动态绑定机制,调用 B类 的该方法
return getI() + 10;
}
public int sum1() {
// 属性无动态绑定,用的就是本类的属性
return i+10;
}
public int getI() {
return i;
}
}
public class B extends A{
public int i = 20;
@Override
// 注释掉该方法
// public int sum() {
// return i + 20;
// }
@Override
public int getI() { // 该方法返回本类的属性 i ,即 20
return i;
}
@Override
// public int sum1() {
// return i + 10;
// }
}
// 然用写一个测试类
public class Test {
public static void main(String[] args) {
A a = new B(); // 向上转型
// a 的运行类型是 B
// B类 的sum()方法已经注释,所以这里调用的是 A类 的sum()方法
// 在sum()中又调用了共有的getI()方法,触发动态绑定,使用B类的getI()
System.out.println(a.sum()); // 30
// B类 的sum1()方法已经注释,所以这里调用的是 A类 的sum1()方法
System.out.println(a.sum1()); // 20
}
}
== 运算符 与 equals()方法
== 运算符可以用来判断基本类型是否相等,以及引用类型的地址是否相等
- 如对于基本类型
3 == 4 // false
,10.0 == 10 // true
- 对于引用类型,假设有类 Man 的两个对象 a , b, 只有两个对象指向同一块内存,即地址相同时返回 true
equals()方法用来判断引用类型的地址是否相等。它是 Object 类的方法。
但是其子类往往重写了该方法,比如字符串String的源码:
public boolean equals(Object anObject) {
if (this == anObject) { // 指向的地址若相同
return true;
}
if (anObject instanceof String) { // 是字符串
String anotherString = (String)anObject; // 向下转型
int n = value.length;
if (n == anotherString.value.length) { // 挨个对比
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false; // 不是字符串直接false
}
再看一下Integer的源码:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
进行一个测试:
// 创建两个Integer对象
Integer c = new Integer(1000);
Integer d = new Integer(1000);
System.out.println(c == d); // false 因为两个对象的地址不同
System.out.println(c.equals(d)); // true 值相同
hashCode()方法
该方法是Object类自带的方法,作用是返回某个对象的哈希码值。
- 哈希码不同的对象一定不是同一个对象,即同一个对象的哈希码一定相同
- 不同的对象哈希码可能相同,正常情况下都是不相同的
toString()方法
Object 也自带,基本类型一般会重写这个方法
在 Object 类中,它是用来返回调用对象的全类名(包名+ 类名)加上该对象的哈希值
若使用输出语句直接输出某个对象,而不指定属性或者方法,将会默认调用它的 toString() 方法
源码:
// getClass().getName() 获取全类名
// Integer.toHexString(hashCode()) 是将该对象的哈希码值转为16进制
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
finalize()
Object 类自带的方法。当某个对象不再有任何引用时,则 JVM 就会认为这是一个垃圾对象对其进行垃圾回收,在销毁前会先调用它的 finalize() 方法。
于是,可以通过重写该方法,在垃圾回收的时候填上自己需要的业务逻辑(如打开关闭其它文件,数据库操作,释放资源等)
不过需要注意,JVM有自己的垃圾回收算法,不是某个对象没有引用就会立即回收,何时回收是由系统决定的。但程序员也可以通过 System.gc() 主动触发垃圾回收机制(不是立即触发,也不是百分百触发,可能系统正好有别的安排)。
事实上,基本很少使用。只是面试会需要
契约设计
java编程设计遵循契约精神。
契约:规定规范了对象应该包含的行为方法。
接口定义了方法的名称、参数和返回值,规范了派生类的行为。
基于接口,利用转型和多态,不影响真正方法的调用,成功将调用类和被调用类解耦。
这里使用一个例子,是上一个多态例子的补充。
// 调用类
haveLunch(new Cat()); // Animal a = new Cat(); haveLunch(a);
haveLunch(new Dog());
haveLunch(
new Animal() { // 使用Animal接口的匿名类,必须实现接口所有方法
public void eat() {
System.out.println("Anonymous class can eat!");
}
public void move() {
System.out.println("Anonymous class can move!");
}
}
);
// 被调用类
public static void haveLunch(Animal a) {
a.eat();
}
调用类调用haveLunch,只需要传进来一个实现Animal接口的对象。
这样被调用类只用关心自己接收的是一个实现Animal接口的对象,而不关心对象本身的内容,只用按照接口的规则来开发实现自己的内容。
而调用类是实现接口的某个类,在被调用类执行时,这里有个向上转型的过程,a.eat()调用的其实是调用类的eat()方法。
根据调用类实现方法的不同,被调用类的执行结果也不同。
从而实现解耦。
Tips:
- double是浮点数,不精确,不能用于switch语句中
- main方法可以被重载、调用、继承和隐藏
- main方法的形参名字可以更改,但类型不可以更改
- JDK包含JRE,JDK是开发工具包,JRE是运行环境
- 接口所有方法都不能实现,且没有构造函数和main函数
- 抽象类可以有main函数
- 静态成员变量通常也叫做类变量,因为它不从属于任何对象。非静态的成员变量,也叫做实例变量,是从属于某一个具体的对象。
- 三元运算符是一个整体。如果有语句:
Object obj = true ? new Integer(1) : new Double(2.0)
,然后对 obj 输出,结果会是 1.0。因为三元运算符是一个整体,类型上 Double 的优先级要高于 Integer,所以就给 1 转换成了 1.0 显示