Java基础系列文章-数据类型之引用数据类型

Java基础系列文章-数据类型之引用数据类型

Java基础系列文章-数据类型之引用数据类型



前言

这篇博客主要记录一些对Java引用数据类型的理解


提示:以下是本篇文章正文内容,下面案例可供参考

一、数据类型结构图

示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。

二、引用数据类型

1.类(Class)

类可以看成是创建 Java 对象的模板,它描述一类对象的行为和状态。

通过下面一个简单的类来理解下 Java 中类的定义:

public class Dog {
    String breed;
    int size;
    String colour;
    int age;
 
    void eat() {
    }
 
    void run() {
    }
 
    void sleep(){
    }
 
    void name(){
    }
}

一个类可以包含以下类型变量:

  • 局部变量:在方法、构造方法或者语句块中定义的变量被称为局部变量。变量声明和初始化都是在方法中,方法结束后,变量就会自动销毁。
  • 成员变量:成员变量是定义在类中,方法体之外的变量。这种变量在创建对象的时候实例化。成员变量可以被类中方法、构造方法和特定类的语句块访问。
  • 类变量:类变量也声明在类中,方法体之外,但必须声明为 static 类型。

一个类可以拥有多个方法,在上面的例子中:eat()、run()、sleep() 和 name() 都是 Dog 类的方法。

1.1构造方法

每个类都有构造方法。如果没有显式地为类定义构造方法,Java 编译器将会为该类提供一个默认构造方法。

在创建一个对象的时候,至少要调用一个构造方法。构造方法的名称必须与类同名,一个类可以有多个构造方法。

下面是一个构造方法示例:

public class Nice{
    public Nice(){
    }
 
    public Nice(String name){
        // 这个构造器仅有一个参数:name
    }
}

1.2创建对象

对象是根据类创建的。在Java中,使用关键字 new 来创建一个新的对象。创建对象需要以下三步:

声明:声明一个对象,包括对象名称和对象类型。
实例化:使用关键字 new 来创建一个对象。
初始化:使用 new 创建对象时,会调用构造方法初始化对象。
下面是一个创建对象的例子:

public class Puppy{
   public Puppy(String name){
      //这个构造器仅有一个参数:name
      System.out.println("小狗的名字是 : " + name ); 
   }
   public static void main(String[] args){
      // 下面的语句将创建一个Puppy对象
      Puppy myPuppy = new Puppy( "tommy" );
   }
}

编译并运行上面的程序,会打印出下面的结果:

小狗的名字是 : tommy

1.3访问实例变量和方法

通过已创建的对象来访问成员变量和成员方法,如下所示:

/* 实例化对象 */
Object referenceVariable = new Constructor();
/* 访问类中的变量 */
referenceVariable.variableName;
/* 访问类中的方法 */
referenceVariable.methodName();

1.4实例

下面的例子展示如何访问实例变量和调用成员方法:

public class Puppy{
   int puppyAge;
   public Puppy(String name){
      // 这个构造器仅有一个参数:name
      System.out.println("小狗的名字是 : " + name ); 
   }
 
   public void setAge( int age ){
       puppyAge = age;
   }
 
   public int getAge( ){
       System.out.println("小狗的年龄为 : " + puppyAge ); 
       return puppyAge;
   }
 
   public static void main(String[] args){
      /* 创建对象 */
      Puppy myPuppy = new Puppy( "tommy" );
      /* 通过方法来设定age */
      myPuppy.setAge( 2 );
      /* 调用另一个方法获取age */
      myPuppy.getAge( );
      /*你也可以像下面这样访问成员变量 */
      System.out.println("变量值 : " + myPuppy.puppyAge ); 
   }
}

编译并运行上面的程序,产生如下结果:

小狗的名字是 : tommy
小狗的年龄为 : 2
变量值 : 2

1.5源文件声明规则

在本节的最后部分,我们将学习源文件的声明规则。当在一个源文件中定义多个类,并且还有import语句和package语句时,要特别注意这些规则。

  • 一个源文件中只能有一个 public 类
  • 一个源文件可以有多个非 public 类
  • 源文件的名称应该和 public 类的类名保持一致。例如:源文件中 public 类的类名是 Employee,那么源文件应该命名为Employee.java。
  • 如果一个类定义在某个包中,那么 package 语句应该在源文件的首行。
  • 如果源文件包含 import 语句,那么应该放在 package 语句和类定义之间。如果没有 package 语句,那么 import 语句应该在源文件中最前面。
  • import 语句和 package 语句对源文件中定义的所有类都有效。在同一源文件中,不能给不同的类不同的包声明。

类有若干种访问级别,并且类也分不同的类型:抽象类和 final 类等。这些将在访问控制章节介绍。

除了上面提到的几种类型,Java 还有一些特殊的类,如:内部类、匿名类。

1.6 Java 包

包主要用来对类和接口进行分类。当开发 Java 程序时,可能编写成百上千的类,因此很有必要对类和接口进行分类。

1.7 import 语句

在 Java 中,如果给出一个完整的限定名,包括包名、类名,那么 Java 编译器就可以很容易地定位到源代码或者类。import 语句就是用来提供一个合理的路径,使得编译器可以找到某个类。

例如,下面的命令行将会命令编译器载入 java_installation/java/io 路径下的所有类

import java.io.*;

一个简单的例子
在该例子中,我们创建两个类:Employee 和 EmployeeTest。

首先打开文本编辑器,把下面的代码粘贴进去。注意将文件保存为 Employee.java。

Employee 类有四个成员变量:name、age、designation 和 salary。该类显式声明了一个构造方法,该方法只有一个参数。

Employee.java 文件代码:

import java.io.*;
public class Employee{
   String name;
   int age;
   String designation;
   double salary;
   // Employee 类的构造器
   public Employee(String name){
      this.name = name;
   }
   // 设置age的值
   public void empAge(int empAge){
      age =  empAge;
   }
   /* 设置designation的值*/
   public void empDesignation(String empDesig){
      designation = empDesig;
   }
   /* 设置salary的值*/
   public void empSalary(double empSalary){
      salary = empSalary;
   }
   /* 打印信息 */
   public void printEmployee(){
      System.out.println("Name:"+ name );
      System.out.println("Age:" + age );
      System.out.println("Designation:" + designation );
      System.out.println("Salary:" + salary);
   }
}

程序都是从main方法开始执行。为了能运行这个程序,必须包含main方法并且创建一个实例对象。

下面给出EmployeeTest类,该类实例化2个 Employee 类的实例,并调用方法设置变量的值。

将下面的代码保存在 EmployeeTest.java文件中。

EmployeeTest.java 文件代码:

import java.io.*;
public class EmployeeTest{

   public static void main(String args[]){
      /* 使用构造器创建两个对象 */
      Employee empOne = new Employee("James Smith");
      Employee empTwo = new Employee("Mary Anne");

      // 调用这两个对象的成员方法
      empOne.empAge(26);
      empOne.empDesignation("Senior Software Engineer");
      empOne.empSalary(1000);
      empOne.printEmployee();

      empTwo.empAge(21);
      empTwo.empDesignation("Software Engineer");
      empTwo.empSalary(500);
      empTwo.printEmployee();
   }
}

编译这两个文件并且运行 EmployeeTest 类,可以看到如下结果:

C :> javac Employee.java
C :> vi EmployeeTest.java
C :> javac  EmployeeTest.java
C :> java EmployeeTest
Name:James Smith
Age:26
Designation:Senior Software Engineer
Salary:1000.0
Name:Mary Anne
Age:21
Designation:Software Engineer
Salary:500.0

1.8 类图

1.8.1 泛化关系(泛化)

类的继承结构在UML中为:泛化(generalize)与实现(realize):

继承关系为is-a的关系;两个对象之间如果可以用is-a来表示,就是继承关系:(…是…)

例如:自行车是车、猫是动物

泛化关系用带空心箭头的一条直接表示;如下图表示(Car和Trunck继承自Vihical);

在这里插入图片描述

注:最终代码中,泛化关系可叠加非抽象类;

1.8.2 实现关系(realize)

实现关系用一条带空心箭头的虚线表示;

例如:“移动行为”作为一个抽象概念,在实际中并使用定义对象;只有指定具体的子类(飞行还是跑步),才可以定义对象(“移动行为”这个类在C++中用抽象类表示,在JAVA中有接口这个概念,更容易理解)

在这里插入图片描述

注:最终代码中,实现关系到继承抽象类;

1.8.3 聚合关系(聚合)

聚合关系用带心菱形箭头的一条直线表示,如图表示键盘、鼠标、显示屏聚合到电脑上,或者电脑由键盘、鼠标、显示屏组成;

在这里插入图片描述

聚合关系表示实体对象的整体关系由多个组成部分组成;

与组合部门关系不同的是,整体和部分不是强依赖的,即使整体不存在,部分仍然存在;例如,不会撤销,人员消失,他们仍然存在;

1.8.4 组合关系(组合)

组合关系用带实心菱形由一条直线表示,和聚合不同,组合中整体和部分是强依赖的,整体不存在了部分也不存在了。比如公司和部门,公司没了部门就不存在了。但是公司和员工就属于聚合关系了,因为公司没了员工还在。

在这里插入图片描述

1.8.5 关联关系(association)

之间的关联关系是用不同类型的对象来决定的Spring的、Nature的结构; 所以,关联关系是一种“强关联”的关系;
表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1 对 1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系,一个学校可以有很多学生,但是一个学生只属于一个学校,因此这是一种多对一的关系,在运行开始之前就可以确定。
例如,乘车和车票之间就是一种关联关系;学生和学校就是一种关联关系;
在这里插入图片描述

关联关系不知道突出显示,表示对象之间默认情况;如果特别突出,显示图,表示 A B,但 B 不知道 A;

注:在最终代码中,关联对象通常是成员变量的实现形式;

1.8.6 依赖关系(依赖)

是用一套箭头带的虚线表示的;
关联与关系的不同在于,它是一种临时性的关系,通常在运行期间可能产生,并且随着运行时的变化; 关联关系也发生变化;
很显然,从某种意义上来说,任何结构的变化是一种保持不变的方式,产生应该总是向我们单一的方向,杜绝始终保持不变的;

和关联关系不同的是,依赖关系是在运行过程中起作用的。
A 类和 B 类是依赖关系主要有三种形式:

  • A 类是 B 类中的(某中方法的)局部变量;
  • A 类是 B 类方法当中的一个参数;
  • A 类向 B 类发送消息,从而影响 B 类发生变化;
    在这里插入图片描述

1.9 创建类的四种方式

1.9.1 new的方式创建对象→ 调用了构造函数

这种方式,我们可以调用任意的构造函数(无参的和带参数的)。

User user = new User();
1.9.2 通过反射的方式创建对象→ 调用了构造函数

Java的反射技术是java程序的特征之一,它允许运行中的Java程序对自身进行检查,或者说“自审”,并能直接操作程序的内部属性。
反射的作用:
1)可以通过反射机制发现对象的类型,找到类中包含的方法、属性和构造器
2)可以通过反射创建对象并访问任意对象方法和属性

1.9.2.1 使用Class类的newInstance方法 }

使用Class类的newInstance方法创建对象。这个newInstance方法调用无参的构造函数创建对象。
还是我们之前用过的Dog类,首先JVM利用ClassLoader(类加载器)先将Dog类加载到内存,然后马上产生了一个Class类型的对象,该对象可以看成是一个模型,以后无论创建多少个Dog类的实例都是利用该模型来生成(一个类所对应的Class类型的对象只有一个)。
通过反射创建对象第一步:需要获得class对象

Class clazz = Dog.class;

这样获取到类对象之后就可以通过newInstance()这个方法来获取具体的对象了,需要注意的是这个方法的返回值是Object类型,我们需要进行转型操作

Class clazz = Dog.class;
Dog dog = (Dog)clazz.newInstance();

这样我们就通过反射的方式创建好了java对象,newInstance()和new的区别如下:
newInstance: 弱类型。低效率。只能调用无参构造。
new: 强类型。相对高效。能调用任何public构造。

1.9.2.2 使用Constructor类的newInstance方法 }

和Class类的newInstance方法很像, java.lang.reflect.Constructor类里也有一个newInstance方法可以创建对象。我们可以通过这个newInstance方法调用有参数的和私有的构造函数。

Class clazz=Dog.class;
Constructor constructor=clazz.getConstructor(String.class,int.class});
Dog dog=(Dog) constructor.newInstance("xiaohei",3});
System.out.println(dog.name+" "+dog.age);
1.9.3 使用clone方法 } → 没有调用构造函数

无论何时我们调用一个对象的clone方法,jvm就会创建一个新的对象,将前面对象的内容全部拷贝进去。用clone方法创建对象并不会调用任何构造函数。
要使用clone方法,我们需要先实现Cloneable接口并实现其定义的clone方法。

User user1 = new User(1,"dan");
  User user2 = null;
  user2 = (User) user1.clone();
1.9.4 使用反序列化 } → 没有调用构造函数
  • 序列化:将对象状态转化为可保持或传输的格式的过程,被序列化的对象必须implments Serializable
  • 反序列化:将流转化成对象的过程

当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,即java对象序列,才能在网络上传送,即序列化过程;接收方则需要把字节序列再恢复为java对象,即反序列化。

调用java.io.ObjectInputStream对象的readObject()方法。
当我们序列化和反序列化一个对象,jvm会给我们创建一个单独的对象。在反序列化时,jvm创建对象并不会调用任何构造函数。
为了反序列化一个对象,我们需要让我们的类实现Serializable接口

import java.io.ObjectOutputStream;   
import java.io.ObjectInputStream;   
import java.io.FileInputStream;   
import java.io.FileOutputStream;   
import java.util.Date;   
import java.lang.management.*;   
public class Test {   



  //序列化对象到文件   
  public static void serialize(String fileName){   
    try 
    {   
      //创建一个对象输出流,讲对象输出到文件   
      ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream(fileName));   

      UserInfo user=new UserInfo("renyanwei","888888",20);   
      out.writeObject(user); //序列化一个会员对象   

      out.close();   
    }   
    catch (Exception x)   
    {   
      System.out.println(x.toString());   
    }   

  }   
  //从文件反序列化到对象   
  public static void deserialize(String fileName){   
    try 
    {   
      //创建一个对象输入流,从文件读取对象   
      ObjectInputStream in=new ObjectInputStream(new FileInputStream(fileName));   

      //读取UserInfo对象并调用它的toString()方法   
      UserInfo user=(UserInfo)(in.readObject());        
      System.out.println(user.toString());   

      in.close();   
    }   
    catch (Exception x)   
    {   
      System.out.println(x.toString());   
    }   

  }   

  public static void main(String[] args) {     

    serialize("D:\\test.txt");   
    System.out.println("序列化完毕");   

    deserialize("D:\\test.txt");   
    System.out.println("反序列化完毕");   
  }   

}  

2.接口(Interface)

2.1 什么是接口?

Java中的接口定义了一个引用类型来创建抽象概念。接口由类实现以提供概念的实现。

在Java 8之前,一个接口只能包含抽象方法。 Java 8允许接口具有实现的静态和默认方法。

接口通过抽象概念定义不相关类之间的关系。

例如,我们可以创建一个Person类来表示一个人,我们可以创建一个Dog类来表示一只狗。

人和狗都可以走路。这里的步行是一个抽象的概念。狗可以走,人也是这样。这里我们可以创建一个名为Walkable的接口来表示walk的概念。然后我们可以有Person类和Dog类来实现Walkable概念并提供自己的实现。 Person类实现了Walkable接口,并使人以人的方式走路。Dog类可以实现Walkable界面,使狗以狗的方式走路。

2.2 接口与类相似点

  • 一个接口可以有多个方法。
  • 接口文件保存在 .java 结尾的文件中,文件名使用接口名。
  • 接口的字节码文件保存在 .class 结尾的文件中。
  • 接口相应的字节码文件必须在与包名称相匹配的目录结构中。

2.3 接口与类的区别

  • 接口不能用于实例化对象。
  • 接口没有构造方法。
  • 接口中所有的方法必须是抽象方法,Java 8 之后 接口中可以使用 default 关键字修饰的非抽象方法。
  • 接口不能包含成员变量,除了 static 和 final 变量。
  • 接口不是被类继承了,而是要被类实现。
  • 接口支持多继承。

2.4 接口特性

  • 接口中每一个方法也是隐式抽象的,接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)。
  • 接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量(并且只能是 public,用 private 修饰会报编译错误)。
  • 接口中的方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。

2.5 抽象类和接口的区别

  • 抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行。
  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。
  • 接口中不能含有静态代码块以及静态方法(用 static 修饰的方法),而抽象类是可以有静态代码块和静态方法。
  • 一个类只能继承一个抽象类,而一个类却可以实现多个接口。

2.6 声明接口

接口有以下特性:

  • 接口是隐式抽象的,当声明一个接口的时候,不必使用abstract关键字。
  • 接口中每一个方法也是隐式抽象的,声明时同样不需要abstract关键字。
  • 接口中的方法都是公有的。
<modifiers> interface <interface-name>  { 
    Constant-Declaration
    Method-Declaration
    Nested-Type-Declaration
}

接口声明以修饰符列表开头,可能为空。
像类一样,一个接口可以有一个公共或包级别的作用域。
关键字public用于指示接口具有公共范围。
缺少范围修饰符指示接口具有包级别作用域。具有包级别作用域的接口只能在其包的成员内引用。
关键字interface用于声明接口,后面是接口的名称。
接口的名称必须是有效的Java标识符。
接口体跟在其名称后面并放在大括号内。
接口的主体可以为空。以下是最简单的接口声明:

interface CRUDdatable  {
    // The interface body  is empty
}

像类一样,一个接口有一个简单的名称和一个完全限定名。关键字interface后面的标识符是其简单名称。
接口的完全限定名称通过使用其包名称和用点分隔的简单名称形成。
在上面的示例中,CRUDdatable是简单的名称,com.java2s.CRUDdatable是完全限定名称。
使用接口的简单和完全限定名的规则与类的规则相同。
下面的代码声明一个名为ReadOnly的接口。它有一个公共范围。

package  com.w3cschool;

public interface  ReadOnly {
    // The interface body  is empty
}

接口声明总是抽象的,无论是否明确声明它是抽象的。

2.7 实现接口

接口指定对象必须提供的协议。
类可以提供接口的抽象方法的部分实现,并且在这种情况下,类必须将自身声明为抽象。
实现接口的类使用“implements”子句来指定接口的名称。
“实现”子句由关键字implements,后跟逗号分隔的接口类型列表组成。
一个类可以实现多个接口。
重写接口中声明的方法时,需要注意以下规则:

  • 类在实现接口的方法时,不能抛出强制性异常,只能在接口中,或者继承接口的抽象类中抛出该强制性异常。
  • 类在重写方法时要保持一致的方法名,并且应该保持相同或者相兼容的返回值类型。
  • 如果实现接口的类是抽象类,那么就没必要实现该接口的方法。

在实现接口的时候,也要注意一些规则:

  • 一个类可以同时实现多个接口。
  • 一个类只能继承一个类,但是能实现多个接口。
  • 一个接口能继承另一个接口,这和类之间的继承比较相似

实现接口的类声明的一般语法如下:

...implements 接口名称[, 其他接口名称, 其他接口名称..., ...] ...

假设有一个Circle类。

public class Circle implements Shape {
   void  draw(){
      System.out.println("draw circle");
   }
}

实现接口的类必须重写以实现接口中声明的所有抽象方法。否则,类必须声明为abstract。
接口的默认方法也由实现类继承。
植入类可以选择不需要重写默认方法。
接口中的静态方法不会被实现类继承。
下面的代码定义了两种引用类型,一种来自Circle类,另一种来自接口类型。

Circle c = new Circle(); 
Shape shape = new Circle();

变量c是Circle类型。它指的是Circle对象。
第二个赋值也是有效的,因为Circle类实现了Shape接口,而Circle类的每个对象也都是Shape类型。

2.7.1 实现接口方法

当一个类完全实现了一个接口时,它为所实现的接口的所有抽象方法提供一个实现。
接口中的方法声明包括方法的约束。例如,方法声明中的throws子句是​​方法的约束。

import java.io.IOException;
interface Shape {
  void draw(double amount) throws IOException;
}
class Main implements Shape{

  @Override
  public void draw(double amount) {
    // TODO Auto-generated method stub
  }  
}

Main的代码是有效的,即使它丢弃了throws子句。当类覆盖接口方法时,允许删除约束异常。
如果我们使用Shape类型,我们必须处理IOException。

import java.io.IOException;

interface Shape {
  void draw(double amount) throws IOException;
}
class Main implements Shape{

  @Override
  public void draw(double amount) {
    // TODO Auto-generated method stub
    
  }
  public void anotherMethod(){
    Shape s = new Main();
    try {
      s.draw(0);
    } catch (IOException e) {
      e.printStackTrace();
    }
    draw(0); 
  }
}
2.7.2 实现多个接口

一个类可以实现多个接口。类实现的所有接口都在类声明中的关键字implements之后列出。

通过实现多个接口,类同意为所有接口中的所有抽象方法提供实现。

interface Adder {
  int add(int n1, int n2);
}
interface Subtractor {
  int subtract(int n1, int n2);
}
class Main implements Adder, Subtractor {
  public int add(int n1, int n2) {
    return n1 + n2;
  }
  public int subtract(int n1, int n2) {
    return n1 - n2;
  }
}
2.7.3 部分实现接口

类不必为所有方法提供实现。

如果一个类不提供接口的完全实现,它必须声明为abstract。

interface Calculator {
  int add(int n1, int n2);

  int subtract(int n1, int n2);
}
abstract class Main implements Calculator{
  public int add(int n1, int n2) {
    return n1 + n2;
  }
}

2.8 接口的继承

一个接口能继承另一个接口,和类之间的继承方式比较相似。接口的继承使用extends关键字,子接口继承父接口的方法。
一个接口使用关键字extends来继承自其他接口。关键字extends之后是以逗号分隔的继承接口名称列表。
继承的接口称为超级接口,继承接口的接口称为子接口。
接口继承其超级接口的以下成员:

  • 抽象和默认方法
  • 常量字段
  • 嵌套类型

接口不从其超级接口继承静态方法。
接口可以重写它从其超级接口继承的继承的抽象和默认方法。
如果超级接口和子接口具有相同名称的字段和嵌套类型,则子接口获胜。
下面的Sports接口被Hockey和Football接口继承:

// 文件名: Sports.java
public interface Sports
{
   public void setHomeTeam(String name);
   public void setVisitingTeam(String name);
}
 
// 文件名: Football.java
public interface Football extends Sports
{
   public void homeTeamScored(int points);
   public void visitingTeamScored(int points);
   public void endOfQuarter(int quarter);
}
 
// 文件名: Hockey.java
public interface Hockey extends Sports
{
   public void homeGoalScored();
   public void visitingGoalScored();
   public void endOfPeriod(int period);
   public void overtimePeriod(int ot);
}

Hockey接口自己声明了四个方法,从Sports接口继承了两个方法,这样,实现Hockey接口的类需要实现六个方法。

相似的,实现Football接口的类需要实现五个方法,其中两个来自于Sports接口。

2.8.1 继承冲突实现

引入默认方法使得类可以从其超类和超级接口继承冲突的实现。

Java使用三个简单的规则为了解决冲突。

  • 超类总是获胜
  • 最具体的超级接口获胜
  • 类必须重写冲突的方法
2.8.2 instanceof运算符

我们可以使用instanceof运算符来评估引用类型变量是指特定类的对象还是其类实现特定接口。
instanceof运算符的一般语法是

referenceVariable instanceof  ReferenceType
interface A {
  default String getValue(){
    return "A";
  }
}
interface B {
  default String getValue(){
    return "B";
  }
}

class MyClass implements B,A{
  public String getValue(){
    return "B";
  }
}

public class Main {
  public static void main(String[] argv){
    MyClass myClass = new MyClass();
    System.out.println(myClass instanceof MyClass);
    System.out.println(myClass instanceof A);
    System.out.println(myClass instanceof B);
  }
}

上面的代码生成以下结果。
true
true
true

2.8.3 接口的多继承

在Java中,类的多继承是不合法,但接口允许多继承。
在接口的多继承中extends关键字只需要使用一次,在其后跟着继承接口。 如下所示:

public interface Hockey extends Sports, Event

以上的程序片段是合法定义的子接口,与类不同的是,接口允许多继承,而 Sports及 Event 可以定义或是继承相同的方法.

2.9 标记接口

最常用的继承接口是没有包含任何方法的接口。
标记接口是没有任何方法和属性的接口.它仅仅表明它的类属于一个特定的类型,供其他代码来测试允许做一些事情。
标记接口作用:简单形象的说就是给某个对象打个标(盖个戳),使对象拥有某个或某些特权。
例如:java.awt.event 包中的 MouseListener 接口继承的 java.util.EventListener 接口定义如下:

package java.util;
public interface EventListener
{}

没有任何方法的接口被称为标记接口。标记接口主要用于以下两种目的:
建立一个公共的父接口:
正如EventListener接口,这是由几十个其他接口扩展的Java API,你可以使用一个标记接口来建立一组接口的父接口。例如:当一个接口继承了EventListener接口,Java虚拟机(JVM)就知道该接口将要被用于一个事件的代理方案。
向一个类添加数据类型:
这种情况是标记接口最初的目的,实现标记接口的类不需要定义任何接口方法(因为标记接口根本就没有方法),但是该类通过多态性变成一个接口类型。

3.数组(Arr[])

数组对于每一门编程语言来说都是重要的数据结构之一,当然不同语言对数组的实现及处理也不尽相同。
Java 语言中提供的数组是用来存储固定大小的同类型元素。
你可以声明一个数组变量,如 numbers[100] 来代替直接声明 100 个独立变量 number0,number1,…,number99。

3.1 声明数组变量

首先必须声明数组变量,才能在程序中使用数组。下面是声明数组变量的语法:

dataType[] arrayRefVar;   // 首选的方法

或

dataType arrayRefVar[];  // 效果相同,但不是首选方法

注意: 建议使用 dataType[] arrayRefVar 的声明风格声明数组变量。 dataType arrayRefVar[] 风格是来自 C/C++ 语言 ,在Java中采用是为了让 C/C++ 程序员能够快速理解 java 语言。

实例
下面是这两种语法的代码示例:

double[] myList;         // 首选的方法double myList[];         //  效果相同,但不是首选方法

3.2 创建数组

Java 语言使用 new操作符来创建数组,语法如下:

arrayRefVar = new dataType[arraySize];

上面的语法语句做了两件事:

一、使用 dataType[arraySize] 创建了一个数组。

二、把新创建的数组的引用赋值给变量 arrayRefVar。

数组变量的声明,和创建数组可以用一条语句完成,如下所示:

dataType[] arrayRefVar = new dataType[arraySize];
//另外,你还可以使用如下的方式创建数组。

dataType[] arrayRefVar = {value0, value1, ..., valuek};

数组的元素是通过索引访问的。数组索引从0开始,所以索引值从 0 到 arrayRefVar.length-1。

那么当数组开辟空间之后,就可以采用如下的方式的操作:

数组的访问通过索引完成,即:“数组名称[索引]”,但是需要注意的是,数组的索引从0开始,所以索引的范围就是0 ~ 数组长度-1,例如开辟了3个空间的数组,所以可以使用的索引是:0,1,2,如果此时访问的时候超过了数组的索引范围,会产生 java.lang.ArrayIndexOutOfBoundsException 异常信息;
当我们数组采用动态初始化开辟空间后,数组里面的每一个元素都是该数组对应数据类型的默认值;
数组本身是一个有序的集合操作,所以对于数组的内容操作往往会采用循环的模式完成,数组是一个有限的数据集合,所以应该使用 for 循环。
在 Java 中提供有一种动态取得数组长度的方式:数组名称.length;
示例: 定义一个int型数组

public class ArrayDemo {
	public static void main(String args[]) {
		int data[] = new int[3]; /*开辟了一个长度为3的数组*/
		data[0] = 10; // 第一个元素
		data[1] = 20; // 第二个元素
		data[2] = 30; // 第三个元素
		for(int x = 0; x < data.length; x++) {
			System.out.println(data[x]); //通过循环控制索引
		}
	}
}

数组本身除了声明并开辟空间之外还有另外一种开辟模式。

示例: 采用分步的模式开辟数组空间

public class ArrayDemo {
	public static void main(String args[]) {
		int data[] = null; 
		data = new int[3]; /*开辟了一个长度为3的数组*/
		data[0] = 10; // 第一个元素
		data[1] = 20; // 第二个元素
		data[2] = 30; // 第三个元素
		for(int x = 0; x < data.length; x++) {
			System.out.println(data[x]); //通过循环控制索引
		}
	}
}

但是千万要记住,数组属于引用数据类型,所以在数组使用之前一定要开辟空间(实例化),如果使用了没有开辟空间的数组,则一定会出现 NullPointerException 异常信息:

public class ArrayDemo {
	public static void main(String args[]) {
		int data[] = null; 
		System.out.println(data[x]);
	}
}

这一原则和之前讲解的对象是完全相同的。

数组在开发之中一定会使用,但是像上面的操作很少。在以后的实际开发之中,会更多的使用数组概念,而直接使用,大部分情况下都只是做一个 for 循环输出。

3.3 处理数组

数组的元素类型和数组的大小都是确定的,所以当处理数组元素时候,我们通常使用基本循环或者 foreach 循环。

示例
该实例完整地展示了如何创建、初始化和操纵数组:

public class TestArray {

   public static void main(String[] args) {
      double[] myList = {1.9, 2.9, 3.4, 3.5};

      // 打印所有数组元素
      for (int i = 0; i < myList.length; i++) {
          System.out.println(myList[i] + " ");
       }
       // 计算所有元素的总和
       double total = 0;
       for (int i = 0; i < myList.length; i++) {
          total += myList[i];
       }
       System.out.println("Total is " + total);
       // 查找最大元素
       double max = myList[0];
       for (int i = 1; i < myList.length; i++) {
          if (myList[i] > max) max = myList[i];
      }
      System.out.println("Max is " + max);
   }
}

以上实例编译运行结果如下:

1.9
2.9
3.4
3.5
Total is 11.7
Max is 3.5

3.4 foreach 循环

JDK 1.5 引进了一种新的循环类型,被称为 foreach 循环或者加强型循环,它能在不使用下标的情况下遍历数组。

语法格式如下:

for(type element: array)
{
    System.out.println(element);
}

示例
该实例用来显示数组 myList 中的所有元素:

public class TestArray {

   public static void main(String[] args) {
      double[] myList = {1.9, 2.9, 3.4, 3.5};

      // 打印所有数组元素
      for (double element: myList) {
         System.out.println(element);
      }
   }
}

以上实例编译运行结果如下:

1.9
2.9
3.4
3.5

3.5 数组作为函数的参数

数组可以作为参数传递给方法。例如,下面的例子就是一个打印 int 数组中元素的方法。

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

下面例子调用 printArray 方法打印出 3,1,2,6,4和2:

printArray(new int[]{3, 1, 2, 6, 4, 2});

3.6 数组作为函数的返回值

public static int[] reverse(int[] list) {
  int[] result = new int[list.length];

  for (int i = 0, j = result.length - 1; i < list.length; i++, j--) {
     result[j] = list[i];
   }
   return result;
 }

以上实例中result数组作为函数的返回值。

3.7 Arrays 类

java.util.Arrays 类能方便地操作数组,它提供的所有方法都是静态的。具有以下功能:

给数组赋值:通过 fill 方法。

对数组排序:通过 sort 方法,按升序。

比较数组:通过 equals 方法比较数组中元素值是否相等。

查找数组元素:通过 binarySearch 方法能对排序好的数组进行二分查找法操作。

具体说明请查看下表:

序号方法和说明
1public static int binarySearch(Object[] a, Object key)用二分查找算法在给定数组中搜索给定值的对象(Byte,Int,double等)。数组在调用前必须排序好的。如果查找值包含在数组中,则返回搜索键的索引;否则返回 (-(插入点) - 1)。
2public static boolean equals(long[] a, long[] a2)如果两个指定的 long 型数组彼此相等,则返回 true。如果两个数组包含相同数量的元素,并且两个数组中的所有相应元素对都是相等的,则认为这两个数组是相等的。换句话说,如果两个数组以相同顺序包含相同的元素,则两个数组是相等的。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)。
3public static void fill(int[] a, int val)将指定的 int 值分配给指定 int 型数组指定范围中的每个元素。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)。
4public static void sort(Object[] a)对指定对象数组根据其元素的自然顺序进行升序排列。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)。

3.8 冒泡排序

 public static void main(String[] args) {
        int[] a = {1,3,6,34,6,8,5,3,3};
        int[] sort = sort(a);
        System.out.println(Arrays.toString(sort));
    }
    //冒泡排序
    //1.比较数组中,两个相邻的元素,如果第一个数比第二个数大,就交换他们的位置;
    //2.每一次比较,都产生一个最大或最小的数;
    //3.下一轮少一次排序;
    public static int[] sort(int[] array){
        //临时变量
        int temp = 0;
 
        //外层循环,判断我们这个要走多少次;
        for (int i = 0; i < array.length-1; i++) {
            boolean flag = false;//通过flag标识位减少没有意义的比较
            //内层循环,比价判断两个数,如果第一个数,比第二个数大,则交换位置
            for (int j = 0; j < array.length-1-i; j++) {
                if (array[j+1]>array[j]){
                    temp = array[j];
                    array[j] = array[j+1];
                    array[j+1] = temp;
                    flag = true;
                }
            }
            if (flag==false){
                break;
            }
        }
        return array;
    }

3.9 稀疏数组

当一个数组(包括多维数组)中的大部分元素为0或者为同一个数值的数组时,为了节约空间起到压缩的效果,将数据用另一种结构来表示,即稀疏数组。
稀疏数组的处理方式:记录数组一共有几行几列,有多少不同值。把具有不同值的元素和行列及值记录在一个小规模的数组中,从而缩小程序的规模。

public static void main(String[] args) {
        //创建二维数组
        int[][] array1 = new int[11][11];
        array1[1][2] = 1;
        array1[2][3] = 2;
        //输出原始数组
        for (int[] ints : array1) {
            for (int anInt : ints) {
                System.out.print(anInt+"\t");
            }
            System.out.println();
        }
        //转换为稀疏数组保存
        int sum = 0;
        for (int i = 0; i < 11 ; i++) {
            for (int j = 0; j < 11 ; j++) {
                if (array1[i][j]!=0) {
                    sum++;
                }
            }
        }
        System.out.println(sum);
 
        //创建一个稀疏数组的数组
        int[][] array2 = new int[sum+1][3];
        array2[0][0] = 11;
        array2[0][1] = 11;
        array2[0][2] = sum;
        //遍历二维数组,将非零的值存放在稀疏数组中。
        int count = 0;
        for (int i = 0; i < array1.length ; i++) {
            for (int j = 0; j < array1[i].length; j++) {
                if (array1[i][j]!=0){
                    count++;
                    array2[count][0] = i;
                    array2[count][1] = j;
                    array2[count][2] = array1[i][j];
                }
            }
        }
        //输出稀疏数组
        for (int i = 0; i < array2.length; i++) {
            System.out.println(array2[i][0]+ "\t"+array2[i][1]+"\t"+array2[i][2]+"\t");
        }
        //还原稀疏数组
        //1.读取稀疏数组
        int[][] array3 = new int[array2[0][0]][array2[0][1]];
        //2.给其中的元素还原它的值
        for (int i = 1; i < array2.length; i++) {
            array3[array2[i][0]][array2[i][1]] = array2[i][2];
        }
        //3.打印
        for (int[] ints : array3) {
            for (int anInt : ints) {
                System.out.print(anInt+"\t");
            }
            System.out.println();
        }
    }

3.10 二维数组

	public static void main(String[] args) {
    int[][] array = {{1,2},{2,3},{3,4},{4,5}};
 
    System.out.println(array[0][0]);
    System.out.println(array[0][1]);
}

//输出结果为
1
2

总结

愿每个人都能坚持自己所热爱的事业。
放弃很简单,但坚持很难。
加油打工人

著作权归NoLongerConfused所有。商业转载请联系NoLongerConfused获得授权,非商业转载请注明出处。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NoLongerConfused

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值