Java基础学习(一)

 Von's Blog     2020-02-28   25985 words    & views

第一个程序

第一个程序显然就是从Hello World开始啦,先来看看Java的Hello World

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

public class Main这句定义了一个名为Main的公共类,public static void main(String[] args)这句具体意义后面再讲,但我感觉可以类比为C中的int main() 。此外,在IDEA中,这句长长的语句可以由快捷指令psvm直接敲出来,而System.out.println("Helllo World!");这句显然就是类似printf的操作啦,而这句长长的命令在IDEA中也能用sout的快捷命令敲出来。

数据类型

Java中的数据类型有两种,基本数据类型和引用数据类型

基本数据类型

基本数据类型总共有四类八种

数据类型 关键字
整数型 byte short int long
浮点型 float double
字符型 char
布尔型 boolean

其中浮点数中默认类型是double,如果一定要使用float类型,需加上一个后缀F。

如果是整数,默认为int类型,如果一定要使用long类型,需加上一个后缀L。

数据类型转换

自动转换

long num = 100;

这段代码是不会报错的,发生了从intlong的自动转换。

自动类型转换可以发生在小范围的数据类型往大范围的数据类型转换之间。如int–>longfloat–>doublelong–>float之间。

强制转换

如这段代码是会报错的

int num = 100L

很显然,因为发生了从大范围类型(long)往小范围类型(int)的转换。此时只能使用强制类型转换。

int num = (int) 100L

byte/short/char这三种类型都可以发生数学运算,如“+”;

byte/short/char这三种类型在运算的时候,都会被首先提升为int类型,然后再计算;

布尔类型不能发生数据类型转换

方法

其实我们在上面已经用过一个方法了:

public static void main(String[] args) {
        System.out.println("Hello World!");
    }

最开始的两个关键字publicstatic的用法我们目前先不讨论,后面的两个参数很显然就是返回值和方法名了,方法例子如下:

public class Main {
    public static void main(String[] args) {
        echo();
    }
    public static void echo(){
        System.out.println("Hello World!");
    }
}

重载(Overload)

重载即:方法名相同,参数列表个数不同或参数类型不同

即方法重载与下列因素相关:

1、参数个数

2、参数类型

3、参数多类型顺序

与下列因素无关:

1、参数的名称

2、方法的返回值类型

其实就是看根据方法名和参数列表能不能唯一定位到一个方法

public class Main {
    public static void main(String[] args) {
        int a = sum(4, 6);
        int b = sum(2, 7, 8);
    }

    public static int sum(int a, int b) {
        return a + b;
    }

    public static int sum(int a, int b, int c) {
        return a + b + c;
    }
}

如这其中两个sum方法即构成了方法的重载,调用时可以自动确认调用的方法。

选择与循环语句

选择语句

if(a > 1){
  a = 20;
}
else if(a == 0){
  a = 2;
}
else {
  a = 100;
}

IDEA常用快捷键(MAC)

快捷键 功能
option+Enter 导入包,自动修正代码
command+删除键 删除光标所在行
command+D 复制光标所在行至光标位置下面
command+option+L 格式化代码
command+/ 单行注释
command+Shift+/ 多行注释
command+N 自动生成代码
command+Shift+上下箭头 移动当前代码行
Shift+Tab 取消缩进

数组

数组初始化

动态初始化

int[] arrayA = new int[100];

使用动态初始化时,数组中的元素将会自动拥有一个默认值。

如果是整数类型,默认为0;

浮点类型默认为0.0;

字符类型默认为'\u0000';

布尔类型默认为false;

引用类型默认为null;

静态初始化

int[] arrayA = {1,2,3};

获取数组长度

int[] arrayA = {1,2,3};
int len = arrayA.length;

数组在程序运行期间,长度不可改变。

遍历数组

int[] array = {1,2,3,4,5,6};
for (int i = 0; i < array.length; i++) {
			System.out.println(array[i]);
}

其中for循环在IDEA中可以使用快捷写法array.fori自动生成(IDEA太强了)

数组作为方法参数和返回值

数组作为方法参数

public static void printArray(int[] array){
    for (int i = 0; i < array.length; i++) {
        System.out.println(array[i]);
    }
}

数组作为方法返回值

public static int[] test(){
    int[] array = {1,2,3};
    return array;
}

类的定义

类有两个组成部分:属性和成员方法

直接来看一个例子,学生类:

public class Student {
    String name;
    int age;
    String num;
    
    public void eat(){
        System.out.println("Eating");
    }
    
    public void learn(){
        System.out.println("Learning");
    }
}

值得注意的几点是

1、成员变量是直接定义在类中的,在方法外边。

2、成员方法不用写static关键字(并不是静态方法)

类的实例化与使用

以上面的学生类为例:

Student s = new Student();
System.out.println(s.age);    //输出0
s.learn();    //输出Learning

可见,类的属性和数组一样,即使我们没有对属性值进行初始赋值,仍会有一个默认值(规则和数组的一样)

如果我们要改变属性的值,只需要:

Student s = new Student();
s.age = 20;
System.out.println(s.age);

对于成员方法来说,属性是类似于其他语言中全局变量的一种存在,可以直接在使用而无需传参

对象作为方法的参数与返回值

对象作为方法参数

public static void main(String[] args) {
  Student s = new Student();
  s.age = 20;
  test(s);    //输出20
}

public static void test(Student s) {
  System.out.println(s.age);
}

对象作为方法返回值

public static void main(String[] args) {
  Student s = test(20);
  System.out.println(s.age);    //输出20
}

public static Student test(int age) {
  Student s = new Student();
  s.age = age;
  return s;
}

成员变量与局部变量的区别

1、定义的位置不一样

局部变量定义在方法的内部,成员变量定义在方法的外部,写在类当中

2、作用范围不一样

局部变量只有在方法中才可以使用,出了方法及不能使用;而成员变量在整个类当中都能使用。

3、默认值不一样

局部变量没有默认值,如果要进行使用必须进行赋值;成员变量有默认值

封装性

封装性是面向对象三大特征之一(面向对象三大特征:封装性、继承、多态

在上面的例子中,我们在类的外部修改属性仅需要一行代码s.age=20,这样无法避免传入非法值(如s.age=-10,年龄显然不能为负值),因此就需要用private关键字来修饰属性,用private修饰的属性在本类中仍然可以直接访问,但是超出本类范围后就不能再直接访问了。

这样的话我们还需要定义一对Getter/Setter方法来访问与设置属性值,具体例子如下:

public class Student {
    String name;
    int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

对年龄进行一定限制的话,可将setAge方法加以限制:

public void setAge(int age) {
  if (age > 0 && age < 100) {
    this.age = age;
  } else {
    System.out.println("Error!");
  }
}

我们在这其中使用了this关键字,this.属性名表示类成员变量,主要是为了避免与局部变量或传参变量冲突,假如我们上面setName写为:

public void setName(String cat) {
        name = cat;
}

也是能起到原有的效果,不过假如我们写为:

public void setName(String name) {
        name = name;
}

将无法被成功赋值(重名以局部变量优先),此时就要用this关键字来唯一指定表示成员变量了。

此外,在IDEA中Getter/Setter方法可以自动生成,在MAC下快捷键是command+N

构造方法

构造方法是专门用来创建对象的方法,当我们通过关键字new来创建对象时,其实就是在调用构造方法。

格式为:

public 类名称(参数类型 参数名称,...){
		方法体
}

构造方法的名称必须和所在类名称完全一样,构造方法不需要返回值(连void都不用),例子:

public Student(String name, int age) {
  this.name = name;
  this.age = age;
}
// Student s = new Student("xwx",18);

构造方法在IDEA中也可以自动生成,在MAC下快捷键也是command+N构造方法也可以进行重载

权限修饰符

关于Java中的权限修饰符其实我们前面已经接触过了,这里来进行较为系统的总结。不同权限修饰符其实区分的就是不同位置的方法能不能访问到该资源(如我们上面提过的private修饰符就能使只有本类访问该对象),详情如下表:

  public protected (Default) private
同一个类
同一个包  
不同包的子类    
不同包且非子类      

一些常用类

Scanner

Scanner类的功能是可以实现键盘输入数据到程序当中。用法如下:

import java.util.Scanner;    //进行导包

public class Main {
    public static void main(String[] args) {
        Scanner s = new Scanner(System.in);    //System.in表示从键盘输入
        int num = s.nextInt();    //s.nextInt()表示从键盘输入的一个整数
        String str = s.next();    //s.next()表示从键盘输入的一个字符串
    }
}

从导包语句中可以看到Scanner类位于java.util下,只有java.lang包下的内容不需要导包,其他的包都需要import语句

匿名对象

匿名对象就是对那些我们确定只需要用到一次的对象,我们可以不大费周章的初始化并命名,而可以进行简写:

int num = new Scanner(System.in).nextInt();

匿名对象作为方法参数和返回值

匿名对象作为方法参数

直接看代码:

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        method(new Scanner(System.in));
    }

    public static void method(Scanner s) {
        System.out.println(s.nextInt());
    }
}

匿名对象作为方法返回值

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner s = method();
        System.out.println(s.nextInt());
    }

    public static Scanner method() {
        return new Scanner(System.in);
    }
}

Random

Random类很显然就是用来生成随机数的类了。

import java.util.Random;

public class Main {
    public static void main(String[] args) {
        Random s = new Random();    //新建一个Random类
        int num = s.nextInt();    //获取一个随机int数字
      	int num1 = s.nextInt(3);    //获取一个位于[0,3)的随机int数字
    }
}

ArrayList

ArrayList类是大小可变的数组的实现,我个人感觉有点类似于Python中的List。和Scanner和Random一样,ArrayList类也位于java.util中,因此需要进行导包操作。

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
    }
}

ArrayList的初始化有一个<>代表泛型,如上是创建了一个内容均为String类型的ArrayList,其中<>里面只能是引用类型,那么假如我们要用ArrayList存储基本数据类型,就必须使用基本数据类型对应的包装类,对应如下:

基本数据类型 包装类
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

向ArrayList中添加数据的方法是add方法

ArrayList<String> list = new ArrayList<>();
list.add("xwx");
list.add("lyc");
System.out.println(list);    //输出[xwx, lyc]

此外,如上面代码所示:直接输出ArrayList中的内容能够像Python的List一样打印出内容,而不会像打印数组一样输出地址。

其他常用方法还有:

public E get(int index)    //从集合中获取元素,参数是索引编号,返回值是对应的元素
public E remove(int index)    //从集合中删除元素,参数是索引编号,返回值是被删除的元素
public int size()    //获取集合的尺寸长度,返回值是集合中元素个数

至于ArrayList的遍历,则和数组的思路没什么区别了:

for (int i = 0; i < list.size(); i++) {
  System.out.println(list.get(i));
}

同理,在IDEA中循环语句也能用list.fori快速生成。

String

字符串类可谓是最常用到的类之一了,String类位于java.lang包下,因此无需进行导包操作。

字符串是常量,字符串的内容永不改变

String的构造方法

  1. public String():该方法可以创建一个空白字符串,不含有任何内容。

    String str = new String();
    
  2. 根据字符数组创建一个字符串

    char[] Array = {'A','B','C'};
    String str = new String(Array);
    
  3. 根据字节数组创建一个字符串

    byte[] Array = {97,98,99};
    String str = new String(Array);
    
  4. 最简单的还有一种我们最常用的直接创建了

    String str = "ABC";
    

字符串的常量池

先来看一个程序:

char[] Array = {'A','B','C'};
String str1 = new String(Array);
String str2 = "ABC";
String str3 = "ABC";

System.out.println(str1 == str2);    //false
System.out.println(str2 == str3);    //true
System.out.println(str3 == str1);    //false

第一句和第三句输出false很好理解,因为引用类型的==比较比较的是地址值(和Python等多种其他语言一样),但是第二句输出为true就比较奇怪了。

这是因为由双引号直接创建的字符串对象位于字符串常量池中,而new出来的字符串对象不在常量池中。

字符串的比较

如上面所示,直接用==比较两个字符串比较的是地址值,假如我们想比较的是字符串的内容,那么就需要用到.equals方法了

char[] Array = {'A','B','C'};
String str1 = new String(Array);
String str2 = "ABC";
System.out.println(str1.equals(str2));    //true

假如想进行不区分大小写的字符串比较,可以用.equalsIgnoreCase方法

char[] Array = {'a','b','c'};
String str1 = new String(Array);
String str2 = "ABC";
System.out.println(str1.equals(str2));    //false
System.out.println(str1.equalsIgnoreCase(str2));    //true

如果比较的双方一个常量一个变量,推荐把常量字符串写在前面,即"ABC".equals(str1)而不是str1.equals("ABC"),因为后者可能会出现null.equals("ABC")导致报错的情况。

与字符串获取相关的方法

  1. 获取字符串长度public int length()

    String str = "ABC";
    System.out.println(str.length());    //3
    
  2. 拼接两个字符串返回一个新字符串public String concat(String str)

    String str = "ABC";
    System.out.println(str.concat("DEF"));    //ABCDEF
    
  3. 获取指定索引位置的单个字符public char charAt(int index)

    String str = "ABCDEF";
    System.out.println(str.charAt(3));    //输出D
    
  4. 查找参数字符串在本字符串中首次出现的索引位置,如果没有返回-1public int indexOf(String str)

    String str = "ABCDEF";
    System.out.println(str.indexOf("DE"));    //输出3
    

与字符串截取相关的方法

public String substring(int index):截取从参数位置一直到字符串末尾,返回新字符串。其还有另外一种重载形式public String substring(int begin,int end):截取从begin开始到end结束中间的字符串[begin,end)

String str = "ABCDEFGHIJKL";
System.out.println(str.substring(4));    //EFGHIJKL
System.out.println(str.substring(3,8));    //DEFGH

与字符串的转换相关的方法

  1. 将当前字符串拆分成字符数组作为返回值public char[] toCharArray()

    String str = "ABCDEFGHIJKL";
    char[] array = str.toCharArray();
    System.out.println(array[0]);    //输出A
    
  2. 将当前字符串拆分成字节数组作为返回值public byte[] getBytes()

    String str = "ABCDEFGHIJKL";
    byte[] array = str.getBytes();
    System.out.println(array[0]);    //输出65
    
  3. 将所有出现的老字符串替换成新字符串,并返回替换之后的新字符串public String replace(Charsequence oldString,CharSequence newString)

    String str = "ABCDEFGHIJKL";
    String str1 = str.replace("A","Hello");
    System.out.println(str1);    //HelloBCDEFGHIJKL
    

与字符串分割相关的方法

按照参数的规则,将字符串切分成若干部分public String[] split(String regex)

String str = "ABCDEF,GHIJKL";
String[] array = str.split(",");
System.out.println(array[0]);    //ABCDEF

后面的参数其实是正则,目前只需要了解假如按照.来切分字符串,需要写为.split("\\.")而不能直接为.split(".")

Static

对于我们上面构造的基本类:

public class Student {
    String name;
    int age;
}

假如我们想添加一个属性,学校,假设所有学生都是同一个学校的,这时这个属性就相当于属于这个类而不是每一个对象了。这时我们就可以使用static关键字了

静态变量

用static修饰的成员变量就是静态变量

public class Student {
    String name;
    int age;
    static String school = "XDU";
}

静态方法

用static修饰的方法就是静态方法,和静态变量一样,静态变量不属于具体的对象,而属于类本身了。因为,其调用方法可以不通过以往的对象名.方法名来调用,而是通过类名称.方法名来调用(用第一种调用方法也可以,不过不推荐),而对于本类中的静态方法,则可以直接省略类名称,直接通过方法名来调用。

public class Student {
    String name;
    int age;
    static String school = "XDU";

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

    public static void test(){
        System.out.println("This is test");
    }
}

从上面这个例子也能看到,静态方法、变量能够在还没实例化对象前就被调用,也就意味着静态方法只能够使用直接静态变量而不能使用普通的成员变量(静态方法和静态变量是最先被初始化的,在类被创建时就被初始化了)

静态代码块

静态代码块格式是

public class Test{
  static {
    //
  }
}

也就是直接在类中用static关键字带上大括号,静态代码块的特点是在第一次用到本类时,静态代码块执行唯一的一次,由于静态内容总是优先于非静态,因此静态代码块比构造方法先执行。

Arrays

Arrays是一个与数组相关的工具类,里面提供了大量静态方法,用来实现数组相关的常用操作。(该包位于java.util下)

public static String toString(数组):将参数数组变成字符串

import java.util.Arrays;

public class Arr {
    public static void main(String[] args) {
        int[] Array = {1,2,3,4};
        String str = Arrays.toString(Array);
        System.out.println(str);    //[1, 2, 3, 4]
    }
}

public static void sort(数组) :按照默认排序(升序)对数组元素进行排序

int[] Array = {12,21,31,14,2};
Arrays.sort(Array);
System.out.println(Arrays.toString(Array));    //[2, 12, 14, 21, 31]

Math

Math这个类从名字就能知道,是和数学相关的一个类。Math是与数学相关的工具类,里面同样提供了大量的静态方法,并且同样位于java.util下面

public static double abs(double num)获取绝对值
public static double ceil(double num)向上取整
public static double floor(double num)向下取整
public static long round(double num)四舍五入

在Math这个类中,还有一个常用到的常量Math.PI,也即近似的圆周率

System.out.println(Math.PI);    //3.141592653589793

继承

继承也是面向对象三大特性之一。java继承的关键字和php一样,都是extends,具体直接看代码:

public class Person {
    public void say_hello(){
        System.out.println("Say Hello!");
    }
}
public class Student extends Person{
    String name;
    int age;
    static String school = "XDU";

    public static void main(String[] args) {
        Student s = new Student();
        s.say_hello();
    }
}

Student类继承了Person类,因此可以直接调用Person类中的方法(子类就是一个父类)。

继承中的成员变量和成员方法访问特点

继承中的成员变量访问特点

直接来看例子:

public class Father {
    int fa = 1;
}
public class Child extends Father{
    int ch =2 ;

    public static void main(String[] args) {
        Child ch = new Child();
        System.out.println(ch.ch);    //输出2
        System.out.println(ch.fa);    //输出1
    }
}

在命名都没重合的情况下可以看到就是正常的访问,那么如果子类和父类有同样名字的成员变量呢?

public class Father {
    int fa = 1;
    int flag = 10;
}
public class Child extends Father{
    int ch =2 ;
    int flag = 100;

    public static void main(String[] args) {
        Child ch = new Child();
        System.out.println(ch.ch);    //输出2
        System.out.println(ch.fa);    //输出1
        System.out.println(ch.flag);    //输出100
    }
}

可以看到,当父类和子类都有相同的成员变量名称时,优先访问子类的成员变量,如果子类中没有该名称的成员变量,则向上去访问父类中的成员变量。

上面这是通过对象.成员变量直接访问成员变量得到的结果,下面来看下通过成员方法间接访问成员变量的访问结果

public class Father {
    int fa = 1;
    int flag = 10;

    public void method_Fa(){
        System.out.println(fa);
    }
}
public class Child extends Father{
    int ch =2 ;
    int flag = 100;

    public static void main(String[] args) {
        Child ch = new Child();
        ch.method_Ch();    //输出2\n100\n1
        ch.method_Fa();    //输出1\n10
    }

    public void method_Ch(){
        System.out.println(ch);
        System.out.println(flag);
        System.out.println(fa);
    }
}

从上面的例子可以看到,通过成员方法调用的成员变量,访问特点是看该成员方法直接属于谁,就优先调用对应类的成员变量,如若没有对应名称的成员变量,则向上寻找父类的成员变量。

super关键字

我们之前区分局部变量和成员变量引入了this关键字,为了区分父类的成员变量也有一个相应的关键字super

public class Father {
    int fa = 1;
    int flag = 10;
}
public class Child extends Father{
    int ch =2 ;
    int flag = 100;

    public static void main(String[] args) {
        Child ch = new Child();
        ch.method_Ch();
    }

    public void method_Ch(){
        System.out.println(this.flag);    //输出100
        System.out.println(super.flag);    //输出10
    }
}

继承中的成员方法访问特点

继承中的成员方法访问特点只有一条,就是:创建的对象是谁,就优先用谁的成员方法,如若没有,就向上找父类的方法。(new的是谁,就优先用谁)

public class Father {

    public void method_Fa(){
        System.out.println("-----method_Father");
    }

    public void method(){
        System.out.println("-----method_1");
    }
}
public class Child extends Father{
    public static void main(String[] args) {
        Child ch = new Child();
        ch.method_Ch();    //输出-----method_Child
        ch.method();    //输出-----method_2
        ch.method_Fa();    //-----method_Father
    }

    public void method_Ch(){
        System.out.println("-----method_Child");
    }

    public void method(){
        System.out.println("-----method_2");
    }
}

覆写(Override)

覆写指的是在继承关系中,方法的名称一样,参数列表也一样.

同时要注意以下两点:

  1. 子类方法的返回值必须小于等于父类方法的返回值范围
  2. 子类方法的权限必须大于等于父类方法的权限修饰符(public > protected > (default) > private

来直接看一个例子:

public class Father {
    
    public void method(){
        System.out.println("-----method_1");
    }
}
public class Child extends Father{

    @Override
    public void method(){
        System.out.println("-----method_2");
    }
}

可以使用@Override来辅助进行是否正确覆写的判断。

继承中构造方法的访问特点

先来看一个例子

public class Father {
    public Father() {
        System.out.println("This is father!");
    }
}
public class Child extends Father{
    public Child() {
        System.out.println("This is child!");
    }
}
public class Main {
    public static void main(String[] args) {
        Child ch = new Child();
    }
}

结果将先输出”This is father!”然后输出”This is child!”,也就是在新建一个子类对象时将先调用父类的构造方法然后调用子类的构造方法(毕竟得先有爸爸然后再有儿子嘛)

但假如我们将其他代码不变,将Father类代码改为:

public class Father {
    public Father(String name){
        System.out.println("This is "+name);
    }
}

此时代码将报错,原因便是当我们没有显式调用的情况下,子类调用的父类构造方法是父类的无参构造方法,而上面我们只声明了一个有参构造的Father类,不含有无参构造方法,也就无法正常运行了。

这里先来回忆下以前的一个知识点,假如我们Father类中什么都不写,代码能不能正常运行呢?

public class Father {

}

答案是可以的,原因我们在之前提过,假如一个类中没有自己去写构造方法,编译器将会赠送一个不包含内容的无参构造方法,而当我们在这个类中定义了任意一个构造方法(不管有参还是无参),这个不包含赠送的无参构造方法将不再被赠送。

那么问题来了,对于上面那种父类只有参构造方法的情况,应该如何调用呢,此时就需要用到我们之前提过的super关键字了,代码如下:

public class Father {
    public Father(String name){
        System.out.println("This is "+name);
    }
}
public class Child extends Father{
    public Child() {
        super("John");
        System.out.println("This is child!");
    }
}
public class Main {
    public static void main(String[] args) {
        Child ch = new Child();
    }
}

在子类中通过super调用了父类的有参构造,其实对于最开始的那种无参构造的情况,本质上相当于

public class Child extends Father{
    public Child() {
        super();
        System.out.println("This is child!");
    }
}

同时最后一点要注意的是,子类有且只能调用一次父类的构造方法 ,并且该语句必须是子类构造方法的第一条语句

关于继承还有最后一个特点:Java中的继承是单继承,即一个类只能有唯一一个直接父类。

抽象

抽象方法和抽象类的引入

假如我们定义了一个图形类Graph,其下有多个子类比如说三角形类Triangle、正方形类Square等,那假如我们想为图形类添加一个方法calculate_area来计算图形的面积,但是显然没有一个通用的方法来计算图形对应的面积(初等数学知识内),只能是具体到三角形是底乘高/2,正方形是边长的平方来具体实现对应的方法编写。

把需求提炼一下即有时我们需要在一个类中定义一个方法,但是我们在这个类中不去对该方法进行实现,而是在其子类中对该方法进行真正的实现,这种方法我们就称之为抽象方法,含有抽象方法的类就称为抽象类。

抽象方法和抽象类的定义

抽象方法和抽象类的定义很简单,抽象方法加abstract然后删除{}及方法体,抽象类直接加abstract关键字就行,直接来看例子:

public abstract class Graph {
    public abstract void calculate_area();
}

抽象方法和抽象类的使用

1、不能直接创建一个抽象类对象:毕竟这个类中还有方法没被具体实现呢(就很抽象的意思)。

2、需要在抽象类的子类中覆写抽象类中的所有抽象方法,然后创建子类对象进行使用。

以上面的Graph类为例子,其使用方法为先定义一个子类Triangle并覆写Graph类中的所有抽象方法:

public class Triangle extends Graph{
    @Override
    public void calculate_area(){
        System.out.println("底乘高/2");
    }
}

然后创建子类对象进行使用:

public class Main {
    public static void main(String[] args) {
        Triangle tr = new Triangle();
        tr.calculate_area();
    }
}

抽象类的注意事项

1、抽象类不一定包含有抽象方法,含有抽象方法的类必定是抽象类

public abstract class Abs {
}

上面这种定义方法是可行的。

2、抽象类的子类中必须覆写抽象类中的所有抽象方法(除非该子类也是抽象类)

接口

接口就是多个类的公共规范,其是一种引用数据类型,最重要的内容便是其中的抽象方法。其基本定义格式如下:

public interface 接口名称 {
  
}

换成关键字interface后,编译生成的字节码文件仍然是.class

不同版本的java可以包含有不同的内容:

Java7:常量、抽象方法

Java8:常量、抽象方法、默认方法、静态方法

Java9:常量、抽象方法、默认方法、静态方法、私有方法

可以看到,Java中接口最多可以包含有5种不同的内容(接口没有静态代码块和构造方法),以下就进行逐一的学习与使用

接口中的抽象方法

接口中抽象方法的定义

接口中的抽象方法定义格式示例如下:

public interface InterfaceAbs {
    public abstract void methodAbs();
}

和之前学的抽象方法定义格式并无不同,不过值得注意的是,前面的修饰符一定是public abstract,不可能是如priviate abstract;因此,public abstract这两个关键字修饰符可以选择性的省略,也即上面的定义等同于:

public interface InterfaceAbs {
    void methodAbs();
}

接口中抽象方法的使用

接口类似于抽象类,不能被直接创建对象,而是应该有一个“实现类”来实现该接口。格式如下:

public class 实现类名称 implements 接口名称 {
  
}

接口的实现类必须覆写(Override)接口中所有的抽象方法,然后创建实现类的对象进行使用就行。

以上面的接口InterfaceAbs为例:

public class InterfaceAbsimpl implements InterfaceAbs{
    @Override
    public void methodAbs() {
        System.out.println("Test.");
    }
}
public class Main {
    public static void main(String[] args) {
        InterfaceAbsimpl in = new InterfaceAbsimpl();
        in.methodAbs();
    }
}

和抽象类的继承关系类似,如果实现类没有覆写接口中的所有抽象方法,那么这个实现类本身就必须是抽象类。

接口中的默认方法

接口中默认方法的定义

从Java 8开始,接口里允许定义默认方法,定义格式如下:

public default 返回值类型 方法名称(参数列表){
  方法体
}

其中public关键字可以选择性省略。

默认方法给我的感觉就是一个正常的方法,毕竟如果都是抽象方法的话,每个子类都要去覆写所有的抽象方法,一旦新增接口的抽象方法,抽象类也就得跟着去进行变换,而默认方法则提供了一个正常的、可以被继承、可覆写可不覆写的方法。

定义例子如下:

public interface InterfaceDef {
    default void method(){
        System.out.println("Test.");
    }
}

接口中默认方法的使用

默认方法的使用仍然是先由一个实现类去实现接口,只不过对于实现类不强制需要去覆写默认方法而已(Mac中生成代码快捷键是^ I)。

public class InterfaceDefimpl implements InterfaceDef{
    @Override
    public void method() {
        System.out.println("This is new method.");
    }
}
public class Main {
    public static void main(String[] args) {
        InterfaceDefimpl in = new InterfaceDefimpl();
        in.method();
    }
}

接口中的静态方法

接口中静态方法的定义

从Java 8开始,接口中允许定义静态方法,定义格式如下:

public static 返回值类型 方法名称(参数列表) {
  方法体
}

类似的,public关键字可以选择性省略。

示例如下:

public interface InterfaceSta {
    static void method(){
        System.out.println("This is static method.");
    }
}

接口中静态方法的使用

和之前我们学到的静态方法一样,接口静态方法是属于接口本身的。因此使用时不需要去创建实现类对象进行调用,直接通过接口名称调用即可

public class Main {
    public static void main(String[] args) {
        InterfaceSta.method();
    }
}

不过在普通类中,对静态方法的调用仍然可以通过对象名称.静态方法这种不规范的调用来进行调用,而在接口中,通过实现类的对象.静态方法这种调用方法是会报错的。

接口中的私有方法

对于接口中有多处复用的代码,我们很容易想到写一个新的方法来封装,但是假如采用默认方法来封装的话,接口的实现类也能调用该默认方法,这并不是我们所希望看到的(因为该方法我们只想供接口中的其他方法调用)。这时我们就能使用私有方法了。

从Java 9开始,接口中允许定义私有方法,接口中的私有方法分为两类:

1、普通私有方法:解决多个默认方法之间重复代码问题,定义格式如下:

private 返回值类型 方法名称(参数列表) {
  方法体
}

2、静态私有方法:解决多个静态方法之间重复代码问题,定义格式如下:

private static 返回值类型 方法名称(参数列表) {
  方法体
}

至于接口中私有方法的使用,便不过是在接口中直接调用方法而已,不再赘述。

接口中的常量

把接口类比为类的话,很容易想到接口中能不能定义”成员变量”呢?接口中定义的”成员变量”其实是一个常量,其定义时必须用public static final三个关键字进行修饰,其效果便等同于常量了;和之前抽象方法、默认方法类似,public staitc final这三个关键字可以选择性省略。

而和普通类的成员变量不同的还有,普通成员变量可以不进行预先赋值(有个默认值),而接口中的常量必须进行赋值,并且一经赋值不允许更改。

从代码规范性来说,接口中的常量命名必须全部大写。

定义及用法如下:

public interface Interf {
    int NUM = 10;
}
public class Main {
    public static void main(String[] args) {
        System.out.println(Interf.NUM);
    }
}

实现多个接口

一个类只能有一个直接父类,但却能实现多个接口。写法如下:

public class MyInterfaceimpl implements MyInterfaceA,MyInterfaceB {
  
}

1、同时实现多个接口需要覆写所有的抽象方法。

2、如果实现类所实现的多个接口中,存在同名的抽象方法,那么只需要覆写一次就可。

3、如果实现类没有覆写所有接口中的所有抽象方法,那么实现类就必须是一个抽象类。

4、如果实现类所实现的多个接口中,存在同名的默认方法,那么实现类一定要对冲突的默认方法进行覆写。

5、一个类可以同时继承一个类并实现接口,示例写法如下:

public class Child extends Father implements Interf{
    //一定要先extends然后再implements
}

一个类如果直接父类中的方法与接口中的默认方法同名,优先用父类中的方法。

接口之间的多继承

接口与接口之间可以继承,并且不同于类之间只能单继承,接口可以有多继承,示例如下:

public interface MyInterface extends FatherInterfaceA,FatherInterfaceB{
  
}

如果多个父接口中的抽象方法重复(返回值、名称一样,参数列表可不同),那么是正常情况。

而如果多个父接口中的默认方法重复,那么子接口就必须进行默认方法的覆写。

多态

多态也是面向对象三大特征之一。代码中体现多态性其实就是父类饮用指向子类对象

父类名称 对象名 = new 子类名称()
或者
接口名称 对象名 = new 实现类名称()

多态中成员的访问特点

首先我们先要明确一点:子类是一个父类,并在父类的基础上增加了自己的一些功能(子类比父类要强),但是你现在创建了一个子类,而把他当成父类来用,那么就相当于束缚住了子类,所以多态中成员的访问特点便是:不能使用子类特有的成员属性和子类特有的成员方法。

多态中的成员方法访问特点

其实和我们之前在普通继承中所学习的一样:创建的对象是谁(new的是谁),就优先用谁的成员方法,如若没有,就向上找父类的方法。(当然,还有不能使用子类特有的成员方法),来看具体例子:

public class Father {
    public void methodFa(){
        System.out.println("This is father.");
    }
    public void method(){
        System.out.println("This is common method----by Father.");
    }
}
public class Child extends Father{
    public void methodCh(){
        System.out.println("This is child.");
    }

    @Override
    public void method(){
        System.out.println("This is common method----by Child.");
    }
}
public class Main {
    public static void main(String[] args) {
        Father obj = new Child();
        obj.method();    //输出This is common method----by Child.
        obj.methodFa();    //输出This is father.
        obj.methodCh();    //错误写法,不允许使用子类特有的成员方法
    }
}

多态中成员变量的访问特点

成员变量的访问有两种形式,一种是通过对象名.成员变量访问得到的成员变量,先来看下这种情况下的访问特点:

public class Father {
    int fa = 1;
    int flag = 10;
}
public class Child extends Father{
        int ch =2 ;
        int flag = 100;
}
public class Main {
    public static void main(String[] args) {
        Father obj = new Child();
        System.out.println(obj.fa);    //输出1
        System.out.println(obj.flag);    //输出10
        System.out.println(obj.ch);    //错误写法,不能使用子类特有的成员属性
    }
}

可以看到通过这种情况下子类的成员变量相当于被舍弃了,即使是父类子类同名的成员变量,访问时也从父类向上找,这种情况下子类成员变量永远不会被调用到。

另一种访问成员变量的方法是通过成员方法访问成员变量(这种访问方法能够突破不能访问子类特有的成员属性的限制),来看下例子:

public class Father {
    int fa = 1;
    int flag = 10;

    public void method_Fa(){
        System.out.println(fa);
        System.out.println(flag);
    }

    public void method(){
        System.out.println("This is Father Common.");
        System.out.println(flag);
    }
}
public class Child extends Father{
        int ch =2 ;
        int flag = 100;

        public void method(){
                System.out.println("This is Child Common.");
                System.out.println(ch);
                System.out.println(flag);
        }
}
public class Main {
    public static void main(String[] args) {
        Father obj = new Child();
        obj.method();    //输出This is Child Common.\n2\n100\n
        obj.method_Fa();    //输出1\n10
    }
}

可以看到,在调用method方法时是调用了Child类的method方法,通过该类的方法访问到了子类中特有的变量ch,也就是我们说的突破了不能访问子类特有的成员属性的限制。而这种情况下访问特点和前面我们所学的普通类继承时通过成员方法访问成员变量访问规则是一致的:访问特点是看该成员方法直接属于谁,就优先调用对应类的成员变量,如若没有对应名称的成员变量,则向上寻找父类的成员变量。

为什么要使用多态?

多态的内容我感觉是比较难理解且抽象的。那么关于为什么要使用多态的问题我觉得教程里面的老师讲得挺好的,附上链接可以方便去重温下:

使用多态的好处

对象的向上转型

对象的向上转型其实就是我们上面的多态写法:父类名称 对象名 = new 子类名称()

对象的向上转型一定是安全的,因为如我们上面所提到的,子类就是一个父类,但是比父类要更”强大”,现在你把一个子类对象当成父类来用,肯定是可以的,这点没什么好说的。

对象的向下转型

对象的向下转型有点像强制类型转换,指的是将父类对象”还原”成其本来的子类对象。

格式为:子类名称 对象名 = (子类名称) 父类对象

假如我们有三个类,Animal类,Cat类,Dog类;其中Cat和Dog是Animal的子类,假如我们出于某种原因使用了多态写法。即:

Animal an = new Cat();

但是后来我们又需要去调用Cat类中特有的成员方法,这时候是无法直接调用对应的方法的(多态的局限性),这时候我们就可以使用对象的向下转型了,将an还原为其本应的Cat对象。

Cat tom = (Cat) an;

如果我们转换时出了差错,要把an还原为Dog对象:

Dog spark = (Dog) an;

此时编译仍能通过,但是运行时会出现异常。

也即我们成功进行对象的向下转型的前提是保证对象本来创建的时候就是对应子类对象才能进行向下转型。

instanceof

instanceof 用于判断一个对象能不能当作某个类型的实例,格式如下:

对象 instanceof 类名称

并返回一个boolean类型。

以上一节的an对象为例

an instanceof Cat

返回True

an instanceof Dog

返回False

final

在Java中final关键字可以用来修饰类、方法、局部变量和成员变量,来分别看下不同的用法

final修饰类

public final class Main {
  
}

被final修饰的类不能有子类

final修饰方法

public class Father {
    int flag = 10;

    public final void method(){
        System.out.println(flag);
    }
}

被final修饰的方法不能被子类覆写

对于类和方法来说,abstract关键字和final关键字不能同时使用(自相矛盾)。

final修饰局部变量

当局部变量用finall修饰时,即表示该变量在初次赋值后无法进行改变。如:

final int num = 10;

但对于引用类型变量,如果用final关键字修饰的话。指的是其地址本身不能被改变,而其地址指向的内容是可以改变的。来看一个具体例子:

public class Person {
    int sex = 1;
}
public class Main {
    public static void main(String[] args) {
        final Person per = new Person();
        per = new Person();    //错误用法,改变了per的地址值
    }
}
public class Main {
    public static void main(String[] args) {
        final Person per = new Person();
        per.sex = 2;    //合法的,改变的是per存储的地址指向的内容中的值
    }

final修饰成员变量

对于成员变量来说,如果使用final关键字修饰,那么这个变量也是在初次赋值后不能进行改变。同时由于成员变量有默认值,所以用了final修饰后必须手动赋值。

对于final修饰的成员变量,要么使用直接赋值,要么通过构造方法赋值(二选一)。同时若是使用构造方法赋值,必须保证类中所有重载的构造方法,都最终会对该成员变量进行赋值。

在实际开发中,一般不单独对成员变量用final修饰,而是用static final对变量进行修饰使之成为一个常量。