Java学习笔记(语言基础及面向对象)

Java语言概述

  • Java虚拟机(JVM : Java Virtual Machine)
    java语言里负责解释执行字节码文件,是运行Java字节码文件的虚拟计算机。
    当使用Java编译器编译Java程序时,生成的是与平台无关的字节码,这些字节码不面向任何具体平台,只面向JVM。
    不同平台上的JVM都是不同的,但他们都提供了相同的接口。
  • Java标准化开发包(JDK : Java SE Development Kit)
    提供了编译、运行Java程序所需的各种工具和资源(包括Java编译器、Java运行时环境以及常用的Java类库等)
  • Java运行时环境(JRE : Java Runtime Enviroment)
    JRE与JVM的关系:JRE包含JVM,JVM是运行Java程序的核心虚拟机,而运行Java程序不仅需要核心虚拟机,还需要其他的类的加载器、字节码校验器以及基础类库。JRE除包含JVM之外,还包含运行Java程序的其他环境支持。
  • 计算机如何查找命令?(设置环境变量的原因)
    Windows根据Path环境变量来查找命令,Path环境变量的值是一系列路径,系统会根据这一系列路径中依次查找命令。如果能找到这个命令,则执行;如果找不到这个命令,就会出现:'xxx’不是内部或外部命令,也不是可运行的程序或批处理文件。
  • 关于CLASSPATH
    如果使用1.4以前版本的JDK,需要设置CLASSPATH环境变量的值为
    .;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar
    一旦设置了该环境变量,JRE将会按照该环境变量指定的路径来搜索Java类。
    点( . )代表当前路径,用以强制Java解释器在当前路径下搜索Java类。
  • Java源文件的命名规则
    如果Java源程序的源代码里定义了一个public类,则该源文件的主文件名必须与public类的类名相同。(因此,一个Java源文件内最多只能有一个public类)
    如果一个源文件内有3个类,则使用javac命令编译会生成3个.class文件,每一个类对应一个.class文件。
  • 提高可读性的建议
  1. 一个Java源文件通常只定义一个类,不同的类使用不同的源文件定义。
  2. 让Java源文件的主文件名与该源文件中定义的public类同名。
  • 垃圾回收(Garbage Collection , GC)
    JRE回收无用内存的机制。通常JRE会提供一个后台线程来进行检测和控制,一般都是在CPU空闲或内存不足时自动进行垃圾回收。
  • Java的堆内存
    是一个运行时数据区,用以保存类的实例。堆内存中存储着在运行的应用程序所建立的所有对象,这些对象不需要程序通过代码来显式地释放。堆内存的回收由垃圾回收器来负责,所有的JVM实现都有一个由垃圾回收器管理的堆内存。

数据类型和运算符

自动提升规则

byte -> short&char -> int -> long -> float -> double

  1. 所有的byte、short和char类型都将被提升到int类型。
short value = 5;
value = value - 2;
//这里的value-2被自动提升到int类型,int赋给short可能会报错
  1. 整个算数表达式的数据类型自动提升到与表达式中最高等级操作数相同的类型
byte b = 40;
var c = 'a';
var i = 23;
var d = .314;
double ans = b + c + i * d;
System.out.print(ans);//输出7

直接量

是指在程序中通过源代码直接给出的值。

int a = 1; //1是直接量
double b = 2.0; //2.0是直接量
String name = "MJT"; //"MJT"是直接量

String类的直接量不能赋给其他类型的变量。
null类型的直接量可以直接赋给任何引用类型的变量,包括String类型。
boolean类型的直接量只能赋给boolean类型的变量,不能赋给其他任何类型的变量。

常量池

指的是在编译器被确定,并保存在已编译的.class文件中的一些数据。
包括关于类、方法、接口中的常量,也包括字符串中的直接量。

//下面的"hello"都在常量池中储存
var s1 = "hello"; 
var s2 = "hello";
var s3 = "he" + "llo";

Java确保每个字符串常量只有一个,不会产生多个副本。
因此,上面那三个"hello"在常量池中是同一块数据。

位运算

运算符 名称 功能
& 按位与 同时为1时返回1
| 按位或 只要有一位为1则返回1
~ 按位非 单目运算符,用来取反。
^ 按位异或 当两位相同时返回0,不同时返回1
<< 左移运算符 右侧补0。左侧超出截断
>> 右移运算符 原来是正数,则左边补零;原来是复数,则左边补1。右侧超出截断
>>> 无符号右移运算符 左侧总是补零。右侧超出截断

n>>x相当于n乘以2的x次方。
n<<x相当于n除以2的x次方。

数组类型

  1. 静态初始化 : 显式指定初始值,系统决定数组长度。
//第一种方式
int[] arr;
arr = new int[] {1,2,3};
//第二种方式(推荐)
var arr = new  int[] {1,2,3};
  1. 动态初始化 : 显式指定长度,系统分配初始值。
//第一种方式
int[] arr = new int[10];
//第二种方式(推荐)
var arr = new int[10];
  1. 动态初始化分配规则
数组元素类型 自动分配的值
整数类型 0
浮点类型 0.0
字符型(char) ‘\u0000’
布尔类型 false
引用类型(类,接口,数组) null

foreach循环

以遍历数组元素为例

var arrs = new int[] {1,2,3};
for (int arr : arrs) {
    System.out.println(arr);
}

使用foreach循环迭代数组元素时,并不能改变数组元素的值,因此不应用foreach的循环变量进行赋值。

深入数组

Java中“数组元素”和“数组变量(引用变量)”在内存中是分开存放的。举个栗子:

int[] p;//定义一个局部变量

这里的p是一个引用变量,作为局部变量被存储在栈内存中。
相当于C++中的指针,指向一个int元素的内存。

p = new int[] {1,2,3};

实际的数组元素{1,2,3}被存储在被new开辟的一块堆内存中,p = new int[] {1,2,3};则让p引用变量指向该堆内存。

二维数组

//动态初始化方式
int[][] arr;
arr = new int[2][];
//静态初始化方式
int[][] arr = new int[][] {new int[2],new int[]{1,2,3}};

这个二维数组实际上完全可以看作是一维数组:使用new int[4]初始化一维数组后,相当于定义了4个int类型的变量;
类似的,使用new int[4][]初始化这个数组后,相当于定义了4个int[]类型的变量,这些int[]类型的变量都是数组类型,因此必须再次初始化这些数组。

面向对象(上)

关于static的问题

为什么静态成员(类成员)不能直接访问非静态成员(对象成员)?

先了解几个书中的基本概念

主调:调用成员变量、方法的对象称为主调。如:主调.方法();

基本概念(1):如果调用static修饰的成员时忽略了主调,那么默认使用该类作为主调。

//在调用static方法时下列两种形式相同。
static_fun();
ClassName.static_fun();

基本概念(2):如果调用没有static修饰的成员时忽略了主调,那么默认使用this作为主调。

//调用普通成员时下列两种形式相同。
fun();
this.fun();

基本概念(3):this指向本类的实例,而static成员是属于类的,因此static成员内不能使用this。

如果在静态成员中调用普通成员,会发生什么? 举个栗子:

public class test
{
    public void show()
    {
        System.out.println("HelloWorld");
    }
    public static void main(String[] args)
    {
        show();
    }
}
===============运行结果=================
错误: 无法从静态上下文中引用非静态 方法 show()

这对应基本概念(2),这里调用的是一个普通成员函数show,则默认使用this作为主调。
这里相当于this.show();
但show()只能通过对象来访问,而static方法是属于类的,在static内的this不知道该指向哪一个实例,因此show()无法在main方法中被调用。

如果确实想在static方法中调用非static方法,可以临时创建一个对象:

public class test
{
    public void show()
    {
        System.out.println("HelloWorld");
    }
    public static void main(String[] args)
    {
        //创建一个临时对象来调用临时对象中的show方法。
        new test().show();
    }
}

方法的参数传递机制

Java参数的传递机制只有一种:值传递

对于引用类型的参数传递,一样采用的是值传递方式。但是这里的“值”,是地址值。比如:

class Data{
    int num1=1;
    int num2=2;
}
public class Test
{
    public static void swap(Data p)
    {
        var temp = p.num1;
        p.num1 = p.num2;
        p.num2 = temp;
    }
    public static void main(String[] args)
    {
        Data a; //定义一个Data类型的引用
        a = new Data(); //a引用指向一块用new新开辟的一块Data内存空间。
        swap(a); //值传递,p接受a的地址值,使得p与a存储着相同的地址值。
        System.out.println(a.num1 + " " + a.num2);
    }
}

形参个数可变的方法

方法:在最后一个形参类型后加三个点 (Typename… name)

public class Test
{
    public static void fun(int num,String... books)
    {
        for (String string : books) {
            System.out.println(string);
        }
    }
    public static void main(String[] args)
    {
        //可直接罗列参数
        fun(3,"Java","C++","Python");
        //也可将String...看做String[],传递一个数组进去。
        fun(3,new String[] {"Java","C++","Python"});
    }
}

必须将Typename…形参放在在最后一个位置,否则会报错。

成员变量和局部变量

成员变量:在类内定义的变量。无需显式初始化,系统自动初始化。
局部变量:在方法内定义的变量。需要显式初始化。

成员变量的初始化和内存中的运行机制

当系统加载类或创建该类的实例时,系统会自动为成员变量分配内存空间。

举个栗子:
比如说我们有一个类

class Person{
    public static int eyeNum;
    public String name;
}

1)如果在主函数中第一次加载这个类:

new Person();

会初始化Person类,只初始化静态变量(类变量)↑

2)然后创建一个Person类型的对象

var p1 = new Person();

创建一个Person对象时并不需要为eyeName类变量分配内存
系统只为name实例变量分配了内存空间

3)再创建第二个Person类对象

var p2 = new Person();
p2.name = "张三";

系统只为第二个对象的对象成员分配了内存空间

局部变量的初始化和内存中的运行机制

int a; //系统并未分配空间
a = 1; //赋值时才分配空间
  1. 局部变量不属于任何类或实例,因此它总是保存在其所在方法的栈内存中
  2. 因为局部变量只保存基本类型的值或者对象的引用,因此局部变量所占的内存区通常比较小

访问控制符

private default protected public
同一个类中
同一个包中
子类中
全局范围内

局部变量不能用用访问控制符修饰

工具方法

只用于辅助该类的其他方法的方法。也应该用private修饰

package包机制

几个知识点

  1. 包机制用于解决类的重名冲突,类文件的管理。
  2. 按照规范,包名最好小写字母。
  3. 应该使用域名的倒写,如com.ccatom包。
  4. package语句必须作为源文件的第一条非注释语句。
  5. 一个源文件只能指定一个包
  6. 同一个包下的类可以直接访问
  7. 通常建议将class文件与java源文件分开放。

特别注意的是:父包内的类使用子包中的类,必须写类的完整包路径加类名。

比如com.ccatom包下有一个类A,com.ccatom.child包下有一个类B。
类A使用类B必须要有com.ccatom.child.B,就像以下形式。

com.ccatom.child.B obj = new com.ccatom.child.B();

一个使用包机制的示例

//定义一个子包的类
package com.ccatom.child;
public class B{
    public static void show(){
        System.out.println("Hello Package");
    }
}
父包类要用子包类,因此先编译子包源文件:javac -d . B.java
发现在以下目录出现了class文件:当前目录\com\ccatom\child\B.class
//再定义一个使用子包类的父包类
package com.ccatom;
public class A{
    public static void main(String[] args){
        //使用完整的名称来使用子类包
        com.ccatom.child.B obj = new com.ccatom.child.B();
        obj.show();
    }
}
编译父包类源文件:javac -d . A.java
发现在以下目录出现了class文件:当前目录\com\ccatom\A.class
运行父类包:java com.ccatom.A
输出Hello Package

如果编译Java文件时不使用-d选项,编译器不会为Java源文件生成相应的文件结构。

import关键字

import关键字用来简化上述源代码,简化以前:

package com.ccatom;
public class A{
    public static void main(String[] args){
        com.ccatom.child.B obj = new com.ccatom.child.B();
        obj.show();
    }
}

使用import直接导入类B,简化之后:

package com.ccatom;
import com.ccatom.child.B;//import导入子包类B
public class A{
    public static void main(String[] args){
        B obj = new B(); //简化了此处的操作
        obj.show();
    }

如果想导入某个包下所有的类,则可以这样写:

import com.ccatom.*; //导入了com.ccatom包下的所有的类

JDK 1.5以后可以进行“静态导入”,用来导入指定类的某个静态成员变量或方法。
如,导入com.ccatom包中类A的静态方法fun() :

import static com.ccatom.A.fun;

也可以导入类A中所有的静态成员:

import static com.ccatom.A.*;

1.import语句应该出现在package语句之后、类定义之前。
2.Java默认为所有源文件导入java.lang包下所有的类,因此可以直接使用String和System类。
使用import可以省略包名;而使用import static则可以连类名都省略。

Java常用包

含有 例如
java.lang 核心类(无需导入) String、Math、System、Thread
java.util 工具类/接口和集合框架类/接口 Arrays、List、Set
java.net 网络编程相关 -
java.text 输入输出的类/接口 -
java.sql JDBC数据库编程的相关类/接口 -
java.awt 抽象窗口工具集,用于构建GUI -
java.swing Swing GUI编程的相关类 -

Java构造器

类似于C++中的构造函数

public class Test{
    public String name;
    public int age;
    //一个简单的构造器,用来初始化name和age
    public Test(String name,int age){
        this.name = name;
        this.age = age;
    }
    public static void main(String[] args){
        Test obj = new Test("MJT",20);
        System.out.println(obj.name+" "+obj.age);
    }
}

不能说Java对象完全由构造器负责创建:在调用构造器之前,系统就已经为对象分配内存空间,并为这个对象执行默认初始化,这个对象就已经产生了。只是这个对象不能被外部访问,只能通过内部构造器的this来引用。

同时,上述代码自定义了一个有参的构造器,这时就不能再用new Test();来创建实例,因为类中没有无参构造器。因此需要进行构造器重载(同一个类里具有多个构造器,各构造器的形参列表不同)

//上述类中重载一个无参的构造器
//就可以用new Test();来创建实例了
public Test(){}
  • 同一个类的构造器之间的调用

一种方法是在构造器中用new关键字来调用另一个构造器,但会重新创建一个对象,占用资源。
因此,对于含有包含关系的构造器来讲,就要用this调用另一个重载的构造器

public Test(String name){
    this.name = name;
}
public Test(String name,int age){
    this(name);//用this调用重载的构造器
    this.age = age;
}

这里需要注意的是:

  1. 使用this调用另一个重载的构造器只能在构造器中使用
  2. 必须作为构造器执行体的第一行语句

类的继承

比如B类以public方式继承A类:

public B extends A{......}

需要注意的一些小知识:

  1. Java摒弃了多继承特征,Java类只能有一个直接父类
  2. 为显式指定父类,则默认父类java.lang.Object,java。因此,java.lang.Object类是所有类的父类
  3. 构造器不能被继承;
  4. 从子类角度看,子类扩展(extends)了父类;
  5. 从父类角度看,父类派生(derive)出了子类;

重写父类的方法

方法重写(override):子类包含父类同名方法的现象。又称方法覆盖

class A{
    public void fun(){
        System.out.println("A_fun");
    }
}
public class B extends A{
    //重写父类A中的方法fun
    public void fun(){
        System.out.println("B_fun");
    }
    public static void main(String[] args){
        B obj = new B();
        obj.fun();//输出B_fun
    }
}

静态方法与非静态方法不能互相重写

如果父类中有一个private方法,则在子类中定义一个同名方法不构成重写,如:

class A{
    //父类中的方法定义为private
    private void fun(){...}
}
class B extends A{
    //这里可以用static限定,因为不属于重写。
    //所定义的fun是子类的新方法
    public static void fun(){...}
}

super限定

如果需要在子类中调用父类中被重写的方法,可以用super限定作为主调来调用被覆盖的方法。比如

class A{
    public int a = 1;
}
public class B extends A{
    public int a = 2;
    public void show_a_in_A(){
        //在子类方法中用super来调用父类的实例变量a
        System.out.println(super.a);
    }
    public static void main(String[] args){
        //尽管父类的a被子类的a覆盖
        //但是new B();的时候依然会为父类中的a开辟一块内存
        B obj = new B();
        obj.show_a_in_A();//输出1
    }
}

需要注意的小知识:

  1. 可以将super与this相似看待,super不能出现在static修饰的方法中
  2. 并不是完全覆盖,系统在创建子类对象时,依然会为父类中定义的、被隐藏的变量分配内存空间;

重载(overload)与重写(override)

重载:发生在同一个类的多个同名方法之前。
重写:发生在子类和父类的同名方法之间。

也有例外:如果子类中定义一个与父类方法有相同的方法名,但参数列表不同的方法,就会形成父类方法和子类方法的重载。

子类调用父类的构造器

同一个类,在一个构造器中调用另一个构造器使用this调用来完成。
类似的,在子类构造器中调用父类的构造器用super调用来完成。

class A{
    private String name;
    public A(String n){
        name = n;
    }
}
public class B extends A{
    private int age;
    public B(String n,int a){
        //使用super限定来调用父类的构造器
        super(n)
        age = a;
    }
    public static void main(String[] args){
        B obj = new B("MJT",20);
    }
}

需要注意的小知识:

  1. super调用父类构造器时也必须出现在构造器执行体的第一行(因此this调用与super调用不会同时出现)
  2. 当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行。
  3. super调用不能用在static方法内(构造器不属于类,也不可能用static修饰)

多态

  • 多态:相同类型的变量,调用同一个方法时呈现出多种不同的行为特征的现象。
  • 编译时类型:由声明变量的类型决定,如 String s ,那么String就是编译时类型。
  • 运行时类型:由实际赋给该变量的对象决定,如var s = new String(); ,那么String就是运行时类型。
  • 向上转型(upcasting):将一个子类对象直接赋给一个父类引用变量

如果编译时类型和运行时类型不一致,就可能出现多态
类似的形式: 编译时类型 变量名 = new 运行时类型();

这种类型的多态分为两种情况:

首先是重写成员的情况:
先上结论:

解释一下图表:变量名.重写方法(); 调用的是执行运行时类型的方法,也就相当于
变量名.执行运行时类型的方法();

以代码为例:

class Father{
    //这里的num与show对应图表中的“重写实例变量”与“重写方法”
    public int num = 1;
    public void show(){
        System.out.println("Father_show_override");
    }
}
public class Child extends Father{
    //子类中重写num实例变量
    public int num = 3;
    //子类中重写show方法
    public void show(){
        System.out.println("Child_show_override");
    }
    public static void main(final String[] args){
        final Father obj = new Child();

        obj.show();//调用重写的show方法
        System.out.println(obj.num);//输出重写的num实例变量
    }
}

然后是特有成员的情况(子类有,父类没有 或 父类有子类没有)
结论:

代码为例:

class Father{
    //父类中特有的实例变量
    public int father_num = 2;
    //父类中特有的方法
    public void father_fun(){
        System.out.println("Father_fun");
    }
}
public class Child extends Father{
    //子类中特有的实例变量
    public int child_num = 4;
    //子类中特有的方法
    public void child_fun(){
        System.out.println("Child_fun");
    }
    public static void main(final String[] args){
        final Father obj = new Child();

        obj.father_fun();//调用父类中特有的方法
        System.out.println(obj.father_num);//输出父类中特有的实例变量

        obj.child_fun();//调用子类中特有的方法,报错
        System.out.println(obj.child_num);//输出子类中特有的实例变量,报错
    }
}

instanceof运算符

  • 使用:双目运算符。前一个操作数通常是一个引用类型变量,后一个操作数通常是一个类(或接口)。

  • 作用:用来判断前面的对象是否是后面的类或其子类(是返回true,不是则返回false)。

  • 应用:一般用于强制类型转换(向上或向下转型)的情景。

    例如:如果试图将一个父类实例强制转换为子类类型,则这个对象必须是子类实例才行(即编译时类型是父类类型,而运行时类型是子类类型)否则将在运行时引发ClassCastException异常。

    通常情况下instanceof和(type)运算符搭配使用:通常先用instanceof判断一个对象是否可以强制类型转换,然后再使用(type)运算符进行强制类型转换,从而保证程序不会出现错误。

    举个栗子:

    //如果obj是String类或其子类
    if(obj instanceof String){
        //那么就可以将obj对象类型强制转换为同类或其父类类型
        var str = (String)obj;
    }
    

使用继承的注意点

  • 尽量隐藏父类的内部数据,用private修饰符。
  • 不要让子类可以随意访问、修改父类的方法。工具方法应该用private修饰;需要被外部类调用,就用public修饰,但又不想子类重写该方法,就再加一个final修饰符;如果希望某个方法被子类重写,又不想被其他类访问,就用protected修饰符。
  • 尽量不要在父类构造器中调用将要被子类重写的方法。

组合

  • 对于继承:子类可以直接获得父类的public方法。
  • 而对于组合:把旧类对象作为新类的成员变量组合起来,用以实现新类的功能。

举个栗子:

class A{
    public void fun_A(){
        System.out.println("Hello_World");
    }
}
class B{
    //将旧类A的对象a作为新类B的成员变量组合起来。
    //这里用private,是因为我们想让用户看到的只是旧类的方法,而不是旧类对象。
    private A a = new A();//组合
    public void fun_A_in_B(){
        //从而可以在新类方法内调用旧类的方法。
        a.fun_A();
    }
}
public class Test{
    public static void main(String[] args){
        var obj = new B();
        obj.fun_A_in_B();
    }
}

组合的内存花销:继承与组合设计的系统开销不会有本质的差别。继承为父类(旧类)开辟空间,也为子类(新类)开辟空间。组合也一样,只不过比继承多了一个引用变量来引用被嵌入的对象,一般影响不大。

初始化块

  • 格式:
[修饰符] {
    //初始化块的可执行性代码
    ......
}
  • 这里的 [修饰符] 如果是static,则称此代码块为类初始化块
  • 没有static修饰,则称为实例初始化块

实例初始化块

  • 实例初始化块只在创建Java对象时隐式执行,而且在构造器执行之前自动执行
public class Test{
    {
        System.out.println("第一个运行的实例初始化块");
    }
    {
        System.out.println("初始化块按顺序执行");
    }
    public Test(){
        System.out.println("先运行完初始化块后运行Test构造器");
    }
    public static void main(String[] args){
        new Test();//创建对象时隐式执行
    }
}
=============执行结果=============
第一个运行的初始化块
初始化块按顺序执行
先运行完初始化块后运行Test构造器

实例初始化块的“假象”:其实在编译后实例初始化块会消失,被还原到每个构造器的所有代码的前面

类初始化块

  • 类初始化块在类初始化阶段执行,而不是创建对象时才执行。因此类初始化块总是比实例初始化块先执行
  • 类初始化块只能访问静态成员。

【重点】实例初始化快、类初始化快、构造器的执行顺序


class Father{
    static{
        System.out.println("父类类初始化块");
    }
    {
        System.out.println("父类实例初始化块");
    }
    public Father(){
        System.out.println("父类的构造器");
    }
}
class Child extends Father{
    static{
        System.out.println("子类类初始化块");
    }
    {
        System.out.println("子类实例初始化块");
    }
    public Child(){
        System.out.println("子类的构造器");
    }
}
public class Test{
    public static void main(String[] args){
        //第一次new Child();
        new Child();
        //第二次new Child();
        new Child();
    }
}

用流程图来简单描述运行流程(贫穷的我只能用试用版亿图图示):

面向对象(下)

包装类

Java中的基本类型功能简单,不具备对象的特性,为了使基本类型具备对象的特性,所以出现了包装类,就可以像操作对象一样操作基本类型数据。

//定义一个包装类Integer的对象
Integer int_obj = new Integer(1024);
  • 自动装箱:将一个基本类型变量直接赋给对应的包装类变量,或者赋给Object变量。
//自动装箱
int num = 1024;//一个基本类型变量
Integer int_obj=num;//直接赋给Integer包装类变量
  • 自动拆箱:与装箱相反,直接把包装类对象赋给一个对应的基本类型变量。
//自动拆箱
Integer int_obj = new Integer(1024);//一个包装类对象
int num = int_obj;//直接赋给基本类型变量
  • 基本数据类型和包装类的对应关系
基本数据类型 包装类
byte Byte
short Short
int Integer
long Long
char Character
float Float
double Double
boolean Boolean
  • 包装类常用方法

打印对象和toString方法

在Java中可以直接输出类对象。
举个栗子:
先定义一个简单的类,用于输出:

class Person{
    private int num;
    public Person(int num){
        this.num = num;
    }
}

在main函数中直接输出Person类对象:

public class Test{
    public static void main(String[] args){
        Person obj = new Person(1024);
        System.out.println(obj);//直接输出类对象
    }
}
============输出结果=============
Person@1f32e575

实际上这里隐式用到了Object类里的一个实例方法toString()
而所有的Java类都是Object类的子类,因此所有的Java对象都具有toString()方法
下面这两种形式是等价的:

System.out.println(obj);
System.out.println(obj.toString());

Object类提供的toString()方法总是返回该对象实现类的"类名+@+hashCode"值。
如果想要输出我们想要的结果,就可以重写toString方法。

class Person{
    private int num;
    public Person(int num){
        this.num = num;
    }
    //重写这个toString方法(Object类的一个实例方法)
    public String toString(){
        return "[" + "num=" + num + "]";//返回我们想要的结果
    }
}

编译javac -d . Test.java
运行java Test
走~~~~

[num=12]

还是挺好玩的QAQ

==和equals方法

在Java中有两种比较变量的方式:一个是利用==,另一个是利用equals()方法。

先来说说==运算符。
这玩意对于基本数据类型可以直接比较,C++中也经常使用。
但是如果用来比较引用类型变量的话,则只有两个引用类型变量都指向同一个对象时,==判断才会返回true。举个栗子:

var num1 = Integer.valueOf(1024);
var num2 = Integer.valueOf(1024);
System.out.println(num1 == num2);
============输出结果=============
false

这里的num1和num2是两个引用类型变量,分别存储着不同的地址,指向不同的位置,==直接判断的是地址,因此输出false

再讲讲equals方法
equals()方法是Object类提供的一个实例方法,因此所有的引用变量都可以调用该方法来判断是否与其他引用变量相等。

var num1 = Integer.valueOf(1024);
var num2 = Integer.valueOf(1024);
System.out.println(num1.equals(num2));
============输出结果==============
true

这里的equals()方法判断的不是地址,而是其指向的值。

自定义equals()方法
这个equals()既然是Object类的一个方法,而Object类是所有类的父类。
那么我们也可以像自定义toString()方法那样来自定义equals()咯~
举个栗子:
我们先定义一个"人"类,有名字也有年龄,只要年龄相等,我们就判断这两个是true
即p1.equals(p2);应该返回true。

class Person{
    private int age;//年龄
    private String name;//姓名
    public Person(int age,String name){
        this.age = age;
        this.name = name;
    }
    //重写equals方法,只判断年龄就好了。
    //重写要求返回值和形参都不能改变。
    public boolean equals(Object obj){
        return (this.age==obj.age?true:false);
    }
}
public class Test{
    public static void main(String[] args){
        //两个人名字不同,但年龄相同。
        Person p1 = new Person(18, "Amy");
        Person p2 = new Person(18, "Tom");
        System.out.println(p1.equals(p2));
    }
}

编译 运行 走~~~~~~

Exception in thread "main" java.lang.Error: Unresolved compilation problem:
    age cannot be resolved or is not a field

    at Person.equals(Test.java:32)
    at Test.main(Test.java:40)

emmm,竟然报错了。
报错信息指向这一段

obj.age

obj作为父类,并没有age这个变量。age是Person子类的特有成员!
但是想要重写,就必须返回值和形参都不能改变,而形参里面必有Object obj(equals文档说的)
那怎么办呢??
强制类型转换
也就是需要进行向下转型,((Person)obj).age。
同时也要判断obj是不是Person类的对象。

public boolean equals(Object obj){
    //判断obj对象是不是Person类对象
    if(obj.getClass() == Person.class){
        return (this.age==((Person)obj).age?true:false);
    }
    return false;
}

其他不变 编译 运行 走~~

true

小细节:为什么是((Person)obj).age而不是(Person)obj.age ?
这是因为成员运算符(.)的优先级要比括号高。
(Person)obj.age就等同于(Person)(obj.age)

final成员变量

  • 前置知识:类初始化时,系统会为该类的类变量分配内存,并分配默认值;当创建对象时,系统会为该对象的实例变量分配内存,并分配默认值。
  • 关于final:用final修饰的成员变量,一旦有了初始值,就不能被重新赋值。
  • Java语法规定:final修饰的成员变量必须由程序员显式地指定初始值。
变量类型 能指定初始值的位置
类变量 静态初始化块、声明时
实例变量 非静态初始化块、声明时、构造器中
/**********类变量*********/

//声明时初始化
final static int num = 1024;
//静态初始化块中初始化
final static int num;
static{
    num = 1024;
}

/**********实例变量*********/

//声明时初始化
final int num = 1024;
//非静态初始化块中初始化
final int num;
{
    num = 1024;
}
//构造器中初始化
final private int num;
public Test(int num){
    this.num = num;
}

特别注意:如果打算在构造器、初始化块中对final成员变量进行初始化,则不要在初始化之前访问final成员变量。

final int num;
{
    //还没有初始化num,这样会报错
    System.out.println(num);
    //但是,Java允许通过方法来访问final变量。
    //因此,通过这个fun()方法来间接访问num
    //会导致系统自动为num赋0
    fun();
    //fun()结束后已经被系统赋初始值了
    //这里的num=1024失效
    num = 1024;
}
public void fun(){
    System.out.println(num);
}

关于final

final局部变量

final局部变量的赋值可以往后稍稍,先声明,过一会再初始化。比如:

public static void main(String[] args){
    final int num;//先声明
    num = 1024;//稍后再进行初始化
    System.out.println(num);

}

final引用类型变量

对于引用类型变量来讲,它保存的仅仅是一个引用,final只保证这个引用类型变量所指向的地址不会改变。

“宏替换”的final变量

对于一个final变量来说,只要满足三个条件:

  1. 使用final修饰符修饰。
  2. 在定义的同时指定了初始值。
  3. 该初始值可以在编译时被确定下来。

这个final变量就不再是一个变量,而是相当于一个直接量,被存储在常量池中。

//下面两个都是final“宏变量”
final var a = 5+2;
final var str1 = "阿腾木"+"的";
final var str2 = "小世界"
//下面的初始值不能在编译时确定
//不被当成宏变量
final var num = Integer.valueOf(1024);

final方法

final修饰的方法不可被重写

一个“例外”:

public class A{
    //这里用了private限定
    //对于B类是不可见的
    final private fun(){}
}
class B extends A{
    //因此这里相当于定义了一个新的方法
    //属于B子类
    public fun(){}
}

final类

final修饰的类不可以有子类

不可变类

不可变(immutable)类的意思是创建该类的实例后,该实例的实例变量是不可改变的。

public class Test{
    //不可变类需要用private和final修饰成员变量
    private final String str;
    //需要提供带参数的构造器
    public Test(String str){
        this.str = str;
    }
    //仅为该类的类成员提供getter方法
    //不提供setter方法
}

不可变类的实例在整个生命周期中永远处于初始化状态。
Java的8个包装类和java.lang.String类都是不可变类

抽象类

  1. 抽象类只能被继承,无法使用new进行实例化。
  2. abstract不能修饰变量。
  3. 有抽象方法的类只能是抽象类,抽象类可以没有抽象方法。
  4. abstract与private不共存,因为在子类中private方法不可见,则无法重写。
  5. final与abstract不能共存,否则类不能被继承,方法不能被重写。
//Abs里面有一个抽象方法,Abs只能是抽象类
public abstract Abs{
    //abstract不能修饰变量
    private String name;
    //抽象类的构造器不能用于创建实例
    //主要用于被其他子类调用
    public Abs(){}
    public Abs(String name){
        this.name = name;
    }
    //抽象方法不提供具体实现,交给子类实现
    public abstract String getName();
}
  • 模板模式:如果编写一个抽象父类,父类提供了多个子类的通用方法,并把一个或多个方法留给子类实现。这种设计模式就叫做模板模式。

抽象类就是从多个具体类中抽象出来的父类
避免了子类设计的随意性

接口

如果说抽象类是从多个类中抽象出来的模板,那么接口抽象的更彻底。
接口是从多个相似类中抽象出来的规范,接口不提供任何实现
接口定义了一批类所需要遵守的规范,接口不关心这些类的内部状态数据,也不关心这些类里方法的实现细节,它只规定这批类里必须提供某些方法,提供这些方法的类就可以满足实际需要。

格式:

[修饰符] interface 接口名 extends 父接口1,父接口2...
{
    各种方法和常量
}

一些知识点:

  1. 修饰符只能是public,要么没有。因为接口是多个类的“公共”规范。
  2. 接口不能有初始化块、构造器。
  3. 接口成员变量只能是静态常量(int a = 1; 则系统自动加public static final修饰)。
  4. 接口的成员变量只能在定义时指定默认值。
  5. 接口的方法只能是类方法、抽象方法(普通方法自动加abstract)、默认方法或私有方法。
  6. 接口中的普通方法(抽象方法)不能有实现;但类方法、默认方法、私有方法必须有方法实现。
public interface Itf{

    /**********成员变量**********/
    int num = 1024;
    //系统自动加public static final修饰
    //必须在定义时指定默认值

    /**********抽象方法**********/
    void fun();
    //系统自动加abstract

    /**********默认方法**********/
    default void def_fun(){
        System.out.println("默认方法");
    }
    //使用default修饰,必须有实现
    //系统自动加public修饰符

    /**********类方法**********/
    static void sta_fun(){
        System.out.println("类方法");
    }
    //系统自动加public修饰符

    /**********私有方法**********/
    private void pri_fun(){
        System.out.println("私有方法");
    }
    //可以使用static修饰符修饰
    //作为工具方法使用
}

一个源文件内只能有一个public接口,主文件名必须与接口名相同。

使用接口

使用格式:

[修饰符] class 类名 extends 父类 implements 接口1,接口2...
{
    ......
}

接口的主要用途:

  1. 定义接口类型的变量,也可用于进行强制类型转换
  2. 调用接口中定义的常量
  3. 接口中的方法被其他类实现

举个栗子,使用上述Itf接口:

public class Test implements Itf{
    //实现接口的抽象方法,需要用public
    //否则会报错:正在尝试分配更低的访问权限; 以前为public
    public void abs_fun(){
        System.out.println("实现了接口中的抽象方法fun");
    }
    public static void main(String[] args){
        Itf obj = new Test();
        System.out.println(obj.num);//调用接口中的常量
        obj.abs_fun();//使用接口的对象调用了实现类所实现的抽象方法
        obj.def_fun();//可以直接调用接口的默认方法
        Itf.sta_fun();//使用接口来调用接口的类方法
    }
}
==========输出结果==========
1024
实现了接口中的抽象方法fun
默认方法
类方法

实现接口方法时,必须使用public修饰符,因为接口里的方法都是public的,而子类重写父类方法时访问权限只能是更大或者相等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章