抽象

抽象的,又叫理论上的。所谓理论上就是实际上没有。java中有抽象类和抽象方法。

抽象类:是使用abstract修饰的类:

 // abstract修饰的抽象类
 public abstract class UniCard {
 }

申明类的目的是规定一类对象的特征和行为。我们可以通过类创建对象。

抽象类就是理论上的类,无法创建对象。

抽象方法:使用abstract修饰的方法。

  • 抽象方法是必须使用abstract修饰的。

  • 抽象方法必须写在抽象类或者接口中。

  • 抽象方法是没有方法体的。

 // abstract修饰的抽象类
 public abstract class UniCard {
     // 抽象方法
     public abstract void withdraw(double money);
 }

所以抽象方法是无法调用的。

抽象方法在抽象类中,抽象方法没有方法体。 无法创建抽象类对象,也无法调用抽象方法。

那么抽象方法有啥用?

比如银联卡UniCard有取钱的方法 withdraw ()

子类:

ICBCCard,重写了withdraw()

ABCCard重写类withdraw()

如果我们要添加别的子类还是要重写withdraw()方法。

而且我们在使用的时候使用的全部是子类对象的。

既然如此,UniCard创建对象就没有意义,UniCard中的方法withdraw()的具体的方法体的实现也是没有意义的。

所以UniCard就非常适合定义为抽象类。方法withdraw()就非常适合定义为抽象方法。

抽象类和抽象方法就好像一个规范, 约定。

`抽象类就是为了让其他类来继承的。当一个类继承了抽象类之后,这个类必须实现抽象类中的抽象方法。`
*当然如果这个子类也是抽象,就可以选择不实现。*

案例:

 public abstract class Vehicle {
     public abstract void move();
 }
 // 机动车
 public abstract class ManeuverVehicle extends Vehicle{
     // 可以不实现父类的抽象方法
     // 可以有自己的抽象方法 加油
     public abstract  void oiling();
 }
 //非机动车
 public abstract class NoManeuverVehicle extends Vehicle{
     // 可以不实现父类的抽象方法
     // 可以有自己的抽象方法 使劲
     public abstract  void manpower();
 }
 public class Car extends ManeuverVehicle {
     // 非抽象子类,必须实现父类的抽象方法
     @Override
     public void oiling() {
         System.out.println("给小汽车加油");
    }
     @Override
     public void move() {
         System.out.println("小汽车在奔驰....");
    }
 }
 public class Truck extends ManeuverVehicle {
     // 非抽象子类,必须实现父类的抽象方法
     @Override
     public void oiling() {
         System.out.println("给卡车加油");
    }
     @Override
     public void move() {
         System.out.println("卡车在奔驰....");
    }
 }
 public class Bicycle extends NoManeuverVehicle{
     @Override
     public void manpower() {
         System.out.println("使劲登....");
    }
     @Override
     public void move() {
         System.out.println("自行车在高速上飞奔...");
    }
 }
 public class Rickshaw extends NoManeuverVehicle {
     @Override
     public void manpower() {
         System.out.println("黄包车--使劲跑...");
    }
     @Override
     public void move() {
         System.out.println("黄包车,在飞奔.....");
    }
 }

准备一个驾驶员:

 public class Driver {
     public void drive(ManeuverVehicle vehicle){
         System.out.println("驾驶机动车..");
         vehicle.oiling();
         vehicle.move();
    }
     public void drive(NoManeuverVehicle vehicle){
         System.out.println("驾驶非机动车...");
         vehicle.manpower();
         vehicle.move();
    }
 }

测试程序测试:

 public class Test {
     public static void main(String[] args) {
         ManeuverVehicle v1 = new Car();
         ManeuverVehicle v2 = new Truck();
         NoManeuverVehicle v3 = new Bicycle();
         NoManeuverVehicle v4 = new Rickshaw();
         Driver d = new Driver();
         d.drive(v1);
         d.drive(v2);
         d.drive(v3);
         d.drive(v4);
    }
 }

抽象类中的几个问题

[1]抽象类中是否可以定义非抽象方法?

抽象类中肯定可以定义非抽象方法。就表示抽象类中也可以定义属性。

 public abstract class Animal {
     private String name;
     public abstract void bark();
     public void breathing(){
         System.out.println(name+"在呼吸");
    }
     public String getName() {
         return name;
    }
     public void setName(String name) {
         this.name = name;
    }
 }

子类:

 public class Dog extends Animal {
     @Override
     public void bark() {
         System.out.println(this.getName() + "在汪汪叫!");
    }
 }

测试:

 public class Test {
     public static void main(String[] args) {
         Animal a = new Dog();
         a.setName("大黄");
         a.bark();
         a.breathing();
    }
 }

设计模式:模版方法设计模式。

描述:生产汽车的流程。统一规定流程:采购零件,组装,喷漆。无论什么汽车都必须按照这个流程来,但是每个汽车的每个流程的实现细节是不一样的。

这时我们就可以定义一个统一的流程:定义一个抽象CreateCar

 public abstract class CreateCar {
     // 采购
     public abstract void purchase();
     // 组装
     public abstract void assemble();
     // 喷漆
     public abstract void painting();
     // 不允许子类重写的方法
     public final void create(){
         System.out.println("准备生产汽车.....");
         // 按照顺序调用上面的三个方法。
         purchase();
         assemble();
         painting();
         System.out.println("生产汽车完成.....");
    }
 }

子类:CreateBugatti

 public class CreateBugatti extends CreateCar {
     @Override
     public void purchase() {
         System.out.println("采购布加迪的零件!");
    }
     @Override
     public void assemble() {
         System.out.println("组装布加迪!");
    }
     @Override
     public void painting() {
         System.out.println("喷上骚气的颜色!");
    }
 }

再来一个子类:

 public class CreateFerrari extends CreateCar{
     @Override
     public void purchase() {
         System.out.println("采购法拉利的零件!");
    }
     @Override
     public void assemble() {
         System.out.println("组装法拉利!");
    }
     @Override
     public void painting() {
         System.out.println("喷上红色闪亮的油漆!");
    }
 }

测试:

 public class Test {
     public static void main(String[] args) {
         CreateCar createCar = new CreateBugatti();
         createCar.create();
         createCar = new CreateFerrari();
         createCar.create();;
    }
 }

[2]抽象类是否有构造方法?

抽象类是有构造方法,而且当抽象类中有了有参数的构造方法之后,子类必须使用super调用父类的有参数的构造方法。

 public abstract class Animal {
     public Animal(String mame){}
 }
 class Dog extends Animal{
     public Dog(String mame) {
         // 必须使用super调用父类的有参数的构造方法
         super(mame);
    }
 }

tips:抽象类即使有构造发那个发,也不能被实例化.

[3]抽象类是否可以继承非抽象类?

抽象类可以继承非抽象类。

[4]抽象类中是否可以定义静态方法?

可以定义。因为静态方法本身就不需要创建这个类对象,就可以调用。

[5]抽象方法是否可以使用static或者final修饰?

抽象方法不能使用static修饰,因为static修饰的方法可以使用类名直接调用,但是抽象方法还没有方法体,无法调用。这就是相矛盾。

抽象方法也不能使用final修饰,因为抽象方法就是为了让子类去重写的。但是final修饰的方法是不能被重写的,这是相矛盾的。

访问修饰符:public 、 protected、 [dufault]、private

其他修饰符:static、final、abstract

java为什么是单继承?

问题:

 public class A{
     public void method(){SOUT("A");}
 }
 public class B{
     public void method(){SOUT("B")}
 }
 public class C extends A,B{}
 C c = new C();
 c.method();// 含糊不清。   这也称之为“菱形继承的问题”。

常用类

常用类就是一些工具类,不需要什么理解,学会使用即可。

JDK的API

java中有很多类都已经写好,可以直接使用。JDK提供了一套文档,用来说明这些类的使用方式。

Math

  • Math类包含执行基本数字运算的方法,如基本指数,对数,平方根和三角函数。

  • Math类中的所有的方法都是静态方法。可以直接使用类名调用。

Math类中的两个常量:

static double E 

       double值比其他任何一个都更接近 e ,自然对数的基数。 

static double PI 

        double值比任何其他的更接近 pi ,圆周长与其直径的比率。

常用的函数:

static double ceil(double a) 返回大于或等于参数的最小(最接近负无穷大) double值,等于一个数学整数。

static double floor(double a) 返回小于或等于参数的最大(最接近正无穷大) double值,等于一个数学整数。

static double max(double a, double b) 有多个重载的方法。 返回两个 double值中的较大值。

static double min(double a, double b) 返回两个 double的较小值。

static double random() 返回值为 double值为正号,大于等于 0.0 ,小于 1.0 。

static long round(double a) 返回参数中最接近的 long ,其中 long四舍五入为正无穷大。

程序案例:

 public class MathTest {
     public static void main(String[] args) {
         System.out.println(Math.E);
         System.out.println(Math.PI);
         System.out.println(Math.ceil(12.5));// 13.0
         System.out.println(Math.floor(12.5));// 12.0
         // 获取随机数
         double i = Math.random();// 返回0~1之间的随机数。
         // 获取0~9之间的随机数
         int x = (int)(i * 10);
         // 1~99之间的随机数
         int y = (int)((Math.random() * 99)+1);
    }
 }

BigDecimal

在数学运算中,有时候就是需要使用double类型进行数学运算。但是我们知道在任何编程语言中,double类型都会出现精度丢失的问题。

 int x = 1;
 double y = 0.99;
 System.out.println(x - y);// 0.010000000000000009

因为计算机无法精准表示0.99。

java提供了BigDecimal解决问题。

  • 不变的,任意精度的带符号的十进制数字。 A BigDecimal由任意精度整数未缩放值和32位整数级别组成 。 如果为零或正数,则刻度是小数点右侧的位数。如果是负数,则数字的非标定值乘以10,以达到等级的否定的幂。 因此,BigDecimal所代表的BigDecimal值为(unscaledValue × 10-scale) 。

提供的方法:

构造方法:

BigDecimal(BigInteger val) 将 BigInteger转换成 BigDecimal 。

 BigDecimal(BigInteger unscaledVal, int scale) 将BigInteger的 BigInteger值和 int等级转换为 BigDecimal 。 

BigDecimal(BigInteger unscaledVal, int scale, MathContext mc) 将 BigInteger未缩放值和 int扩展转换为 BigDecimal ,根据上下文设置进行舍入。

 BigDecimal(BigInteger val, MathContext mc) 根据上下文设置将 BigInteger转换为 BigDecimal舍入。 

BigDecimal(char[] in) 一个转换的字符数组表示 BigDecimal成 BigDecimal ,接受字符作为的相同序列 BigDecimal(String)构造。

 BigDecimal(char[] in, int offset, int len) 一个转换的字符数组表示 BigDecimal成 BigDecimal ,接受字符作为的相同序列 BigDecimal(String)构造,同时允许一个子阵列被指定。

 BigDecimal(char[] in, int offset, int len, MathContext mc) 一个转换的字符数组表示 BigDecimal成 BigDecimal ,接受字符作为的相同序列 BigDecimal(String)构造,同时允许指定一个子阵列和用根据上下文设置进行舍入。

 BigDecimal(char[] in, MathContext mc) 一个转换的字符数组表示 BigDecimal成 BigDecimal ,接受相同的字符序列作为 BigDecimal(String)构造与根据上下文设置进行舍入。

 BigDecimal(double val) 将 double转换为 BigDecimal ,这是 double的二进制浮点值的精确十进制表示。

 BigDecimal(double val, MathContext mc) 将 double转换为 BigDecimal ,根据上下文设置进行舍入。 

BigDecimal(int val) 将 int成 BigDecimal 。 

BigDecimal(int val, MathContext mc) 将 int转换为 BigDecimal ,根据上下文设置进行舍入。

 BigDecimal(long val) 将 long成 BigDecimal 。

 BigDecimal(long val, MathContext mc) 将 long转换为 BigDecimal ,根据上下文设置进行舍入。

 BigDecimal(String val) 将BigDecimal的字符串表示 BigDecimal转换为 BigDecimal 。

 BigDecimal(String val, MathContext mc) 一个转换的字符串表示 BigDecimal成 BigDecimal ,接受相同的字符串作为 BigDecimal(String)构造,利用根据上下文设置进行舍入。

我们一般如何使用呢?

 // 创建BigDecimal对象
 BigDecimal d1 = new BigDecimal(12.5);
 BigDecimal d2 = new BigDecimal(0.99f);
 BigDecimal d3 = new BigDecimal(18);
 BigDecimal d4 = new BigDecimal("0.1");

计算的方法: tips:不可以使用任何算数运算符。

加法运算:

BigDecimal add(BigDecimal augend) 返回 BigDecimal ,其值是 (this + augend) ,其标为 max(this.scale(), augend.scale()) 。

除法运算:

BigDecimal divide(BigDecimal divisor) 返回BigDecimal ,其值为(this / divisor) ,优先级为(this.scale() - divisor.scale()) ; 如果不能表示确切的商(因为它具有非终止的十进制扩展),则抛出一个ArithmeticException 。

乘法运算:

BigDecimal multiply(BigDecimal multiplicand) 返回 BigDecimal ,其值是 (this × multiplicand),其标为 (this.scale() + multiplicand.scale()) 。

取余运算:

BigDecimal remainder(BigDecimal divisor) 返回 BigDecimal ,其值是 (this % divisor) 。

减法运算:

BigDecimal subtract(BigDecimal subtrahend) 返回 BigDecimal ,其值是 (this - subtrahend) ,其标为 max(this.scale(), subtrahend.scale()) 。

比较大小:

int compareTo(BigDecimal val) 将此 BigDecimal与指定的BigDecimal进行 BigDecimal 。

 public class BigDecimalTest {
     public static void main(String[] args) {
         int x = 1;
         double y = 0.99;
         System.out.println(x - y);// 0.010000000000000009
         // 创建BigDecimal对象
         BigDecimal d1 = new BigDecimal(12.5);
         BigDecimal d2 = new BigDecimal(0.99f);
         BigDecimal d3 = new BigDecimal(18);
         BigDecimal d4 = new BigDecimal("0.1");
         BigDecimal dx = new BigDecimal("1.0");
         BigDecimal dy = new BigDecimal("0.99");
         // 加法运算符
         BigDecimal result= dx.add(dy);
         System.out.println("dx + dy = "+(result.doubleValue())); // 1.99
         // 减法
         result = dx.subtract(dy);
         System.out.println("dx - dy = "+(result.doubleValue()));// 0.01
         // 乘法
         result = dx.multiply(dy);
         System.out.println("dx * dy = "+(result.doubleValue()));// 0.99
         // 除法
         result = dx.divide(new BigDecimal("0.25"));
         System.out.println("dx / dy = "+(result.doubleValue()));// 4.0
    }
 }

我们在创建BigDecimal的时候,要尽量传入字符串形式的数字。因为double类型的数字,在传入的时候可能已经丢失了精度。

 BigDecimal d = new BigDecimal("xxxx.xxx");

与BigDecimal相似的还有一个BigInteger

ArrayList

数组:在内存中连续的空间。固定长度

 一个对象数组: Roommate [] roommates = new Roommate[3];

数组特点是连续空间,有索引。所以数组的随机访问的速度特别的快。

数组还有特点固定长度,这也是问题。我们往往希望有一个长度可以变化的数组使用。 实际上是没有的。

所以JDK自己封装了看上去可以改变长度的数组。这个家伙就是ArrayList。

ArrayList是一个类,这个类是JDK中定义好的

tips:这个类是在java.util包下,使用这个类一定要在第一行导包。 import java.util.ArrayList;

构造方法

我们可以创建ArrayList

 // 创建一个ArrayList
 ArrayList a = new ArrayList();

ArrayList常用的API

ArrayList是一个类,它的对象是一个容器。这个容器是可以放数据。ArrayList因为是使用数组实现的,所以每个元素也都有索引,索引也是从0开始。

容器的API:

 添加元素的方法,获取元素个数的方法,获取元素的方法(有索引的可以根据索引,没索引的就使用迭代器)。删除元素的方法,查找元素的方法。

boolean add(E e)

   将指定的元素追加到此列表的末尾 简单认为就是添加一个元素

         ArrayList a = new ArrayList();
         // 默认情况下,集合中的元素类型是可以随意的。
         a.add("大锤");
         a.add("铁锤");
         a.add("铁锅");

void add(int index, E element) 在此列表中的指定位置插入指定的元素。

 // 插入元素
 a.add(1,"如花");
 System.out.println(a);//[大锤, 如花, 铁锤, 铁锅]

void clear() 从列表中删除所有元素。

boolean contains(Object o) 如果此列表包含指定的元素,则返回 true 。 默认情况下这个方法只在基本类型和String这些类型上有效,自定义的类型默认可能有问题。

 boolean f = a.contains("铁锤");
 System.out.println(f);// true

E get(int index) 返回此列表中指定位置的元素。 在没有指定泛型的情况下,从ArrayList中获取的任何类型都会自动转换为Object类型,这时可能需要类型转换。

 // 根据索引获取某一个元素   数组 -- 》 数组名[index]
 Object o = a.get(3);// 获取出来的数据类型统一是Object。
 System.out.println(o.toString());// 铁锅

tips:Object是所有类型的老祖中,任何java类型都可以自动转换为Object类型。我们将任何类型的数据放入ArrayList,最终取出的时候都是Object。我们就可能需要强制类型转换。

int indexOf(Object o) 返回此列表中指定元素的第一次出现的索引,如果此列表不包含元素,则返回-1。

 System.out.println(a);//[大锤, 如花, 铁锤, 铁锅]
 System.out.println(a.indexOf("如花"));// 1
 System.out.println(a.indexOf("小锤锤"));// -1

boolean isEmpty() 如果此列表不包含元素,则返回 true 。

E remove(int index) 删除该列表中指定位置的元素。

Object removeObj = a.remove(3);
System.out.println(removeObj);// 铁锅
System.out.println(a);// [大锤, 如花, 铁锤]

boolean remove(Object o) 从列表中删除指定元素的第一个出现(如果存在)。

a.remove("铁锤");

E set(int index, E element) 用指定的元素替换此列表中指定位置的元素。

System.out.println(a);//[大锤, 如花, 铁锤, 铁锅]
Object setObj = a.set(3, "小锤锤");
System.out.println(setObj);// 铁锅
System.out.println(a);// [大锤, 如花, 铁锤, 小锤锤]

int  size() 返回此列表中的元素数。

System.out.println("集合中的元素个数:"+a.size());
// 遍历集合
for (int i = 0;i < a.size();i ++){
    Object obj = a.get(i);
    System.out.println(i+"--"+obj);
}

List<E> subList(int fromIndex, int toIndex) 返回此列表中指定的 fromIndex (包括)和 toIndex之间的独占视图。

Object[] toArray() 以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。

ArrayList的泛型

任何类型放入ArrayList取出来之后都是Object类型。

public class ArrayListTest2 {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        // 放入整数
        list.add(1);
        // 字符串
        list.add("贰");
        //  对象
        list.add(new Person());
        list.add(new Car());
    }
}
class Person{
    public void showPerson(){
        System.out.println("Person");
    }
}
class Car{
    public void showCar(){
        System.out.println("Car");
    }
}

看图理解:

我们可以通过get方法取出每个位置上的元素:

// 获取这些元素
Object o = list.get(0);
Object o1 = list.get(1);
String str = o1.toString();
//  取出person
Object o2 = list.get(2);
Person p = (Person)o2;
p.showPerson();
//  取出Car
Object o3 = list.get(3);
Car car = (Car)o3;
car.showCar();

ArrayList中的类型默认都会转换为Object类型。

但是实际使用中,其实大概率,我们定义的ArrayList中的数据类型都是一致的。于是乎就有了泛型,可以保证集合中元素的类型安全。

我们可以通过泛型规定这个集合中可以存储的数据类型,这时这个集合只能存储对应类型的元素。取出这个元素时,本身也是类型安全的,不需要强制类型转换。

// 在JDK1.8之前,new ArrayList<String> 中的泛型是必须写的。
// 在JDK1.8之后可以省略
//  String 类型集合
ArrayList<String> names = new ArrayList<String>();
names.add("卡卡西");  //names.add(85);
// 自定义的类型集合
ArrayList<Person> ps = new ArrayList<>();
ps.add(new Person()); //ps.add("大锤");
//取出的时候是类型安全的
Person person = ps.get(0);
 // int类型集合
ArrayList<Integer> ages = new ArrayList<Integer>();
ArrayList<Double> scores = new ArrayList<Double>();

如果集合的泛型是基本类型,则必须使用它们的包装类。

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

ArrayList是如何实现可变长度的数组的

[1]通过源码阅读大致了解了ArrayList底层的变化原理。

[2]看图理解:

当我们使用无参数的构造方法创建一个ArrayList对象的时候。底层只是创建一个对象,并且对象中有个空数组对象。

 ArrayList list = new ArrayList();

当我们放入第一个元素之后:

list.add("零");

继续放元素,放到第十一个元素的时候,就需要扩容,这时做法创建新的数组,长度为原来数组的1.5倍。并且将原来的数组的数据拷贝到新数组中,将新加入的元素放入对应的位置。

list.add("十一");

ArrayList中尽量不要频繁使用的API:

add(int index,E e); 在指定的索引出插入一个元素。

remove(xxx); 删除元素。

如果要修改集合中的元素,建议使用LinkedList。 直接将Linked当作ArrayList器使用。只是名字不一样,其余的用法是一样的。

java集合的家谱图:

String

String对象和基本类型变量区别?

案例:

        // 字符串对象
        String name = "漩涡鸣人";
        // 基本类型变量
        int age = 18;

字符串对象是引用类型,数据存储在堆中。

子串的创建方法:

第一:使用“”直接创建。

第二:使用String的构造方法创建String对象。

常用的String的构造方法:

String() 
	初始化新创建的 String对象,使其表示空字符序列。 
String(byte[] bytes) 
	通过使用平台的默认字符集解码指定的字节数组来构造新的 String 。 
String(byte[] bytes, int offset, int length) 
	通过使用平台的默认字符集解码指定的字节子阵列来构造新的 String 。 
String(char[] value) 
	分配一个新的 String ,以便它表示当前包含在字符数组参数中的字符序列。 
String(char[] value, int offset, int count) 
	分配一个新的 String ,其中包含字符数组参数的子阵列中的字符。
String(String original) 
	初始化新创建的String对象,使其表示与参数相同的字符序列; 换句话说,新创建的字符串是参数字符串的副本。 

看案例:

//" "赋值
String msg = "这是一个字符串";
String str1 = new String("又是一个字符串");
byte [] bs = "依然是一个字符串".getBytes(); //{12,56,-56,-63,-120,125,18,58};
String str2 = new String(bs);
String str3 = new String(bs,4,6);
char [] chs = {'w','h','y','i','s'};
String str4 = new String(chs);
System.out.println(str1);
System.out.println(str2);
System.out.println(str3);
System.out.println(str4);

创建字符串不同方式的区别

字符串是常量。

-字符串是不可改变的。

-字符串在堆内存中的内容是不可修改的。

看程序:

public class StringTest2 {
    public static void main(String[] args) {
        // 使用不同的方式创建相同的字符串
        String str1 = "xiaohei";
        String str2 = "xiaohei";
        String str3 = new String(" xiaohei");
        // == 比较的是栈内存中的内容。
        System.out.println("str1 == str2 : " + (str1 == str2)); // true
        System.out.println("str3 == str2 : " + (str3 == str2));// false
        System.out.println("str1 == str3 : " + (str1 == str3));// false
    }
}

看图理解:

String 是不可改变

所谓不可改变,是说在对内存中的字符序列是不可改变,和平时说的常量是不一样的。

看程序:

public class StringTest3 {
    public static final Object X = new Object();
    public static void main(String[] args) {
        // 这里会出现错误,因为常量不能重新赋值
        X = new Object();
        String str = "你好!";
        // 这里可以正常修改
        str = "你也好!";
    }
}

看图:

字符串的常量是说,堆中的字符序列不能改变,如果试图改变字符串,必然会创建新的字符串。

常量的意思是栈中的内容不能改变,如果是基本类型则数据不能改变。如果引用类型则表示地址不能改变,但是堆中的数据是可以修改的。

下面的案例尽量不要使用String:

// 循环的拼接字符串
String str1 = "";
for (int i = 0; i < 10000;i++){
    str1 += i;// str1 = str1 + i  每次循环都会开辟新的空间,存储新的字符串
}
System.out.println(str1);

上面的情况要尽量使用可变字符串。

String的常用API

char charAt(int index) 
	返回 char指定索引处的值。 
int codePointAt(int index) 
	返回指定索引处的字符(Unicode代码点)。 
int compareTo(String anotherString) 
	按字典顺序比较两个字符串。  
String concat(String str) 
	将指定的字符串连接到该字符串的末尾。  这里是产生一个新串。
boolean contains(CharSequence s) 
	当且仅当此字符串包含指定的char值序列时才返回true。  
boolean equals(Object anObject) 
	将此字符串与指定对象进行比较。  
boolean equalsIgnoreCase(String anotherString) 
	将此 String与其他 String比较,忽略案例注意事项。  
byte[] getBytes() 
	使用平台的默认字符集将此 String编码为字节序列,将结果存储到新的字节数组中。  
int indexOf(int ch) 
	返回指定字符第一次出现的字符串内的索引。  
int indexOf(int ch, int fromIndex) 
	返回指定字符第一次出现的字符串内的索引,以指定的索引开始搜索。  
int indexOf(String str) 
	返回指定子字符串第一次出现的字符串内的索引。  
int indexOf(String str, int fromIndex) 
	返回指定子串的第一次出现的字符串中的索引,从指定的索引开始。  
int lastIndexOf(int ch) 
	返回指定字符的最后一次出现的字符串中的索引。  
int lastIndexOf(int ch, int fromIndex) 
	返回指定字符的最后一次出现的字符串中的索引,从指定的索引开始向后搜索。  
int lastIndexOf(String str) 
	返回指定子字符串最后一次出现的字符串中的索引。  
int lastIndexOf(String str, int fromIndex) 
	返回指定子字符串的最后一次出现的字符串中的索引,从指定索引开始向后搜索。  
int length() 
	返回此字符串的长度。  
String replace(char oldChar, char newChar) 
	返回从替换所有出现的导致一个字符串 oldChar在此字符串 newChar 。   替换之后产生新串。
String[] split(String regex) 
	将此字符串分割为给定的 regular expression的匹配。  
boolean startsWith(String prefix) 
	测试此字符串是否以指定的前缀开头。  
boolean endsWith(String suffix) 
	测试此字符串是否以指定的后缀结尾。 
String toLowerCase() 
	将所有在此字符 String使用默认语言环境的规则,以小写。  
String toUpperCase() 
	将所有在此字符 String使用默认语言环境的规则大写。  
String trim() 
	返回一个字符串,其值为此字符串,并删除任何前导和尾随空格。  
String substring(int beginIndex) 
	返回一个字符串,该字符串是此字符串的子字符串。  
String substring(int beginIndex, int endIndex) 
	返回一个字符串,该字符串是此字符串的子字符串。  

测试代码:

public class StringTest4 {
    public static void main(String[] args) {
        String str = "www.7dianit.com";
        char c = str.charAt(5);
        System.out.println("charAt(5)->"+c);
        int code = str.codePointAt(5);
        System.out.println("codePointAt(5) - >"+code);
        String str1 = "zhangsan";
        String str2 = "lisi";
        System.out.println(str1.compareTo(str2));
        str1 = "旗木";
        str2 = "卡卡西";
        String str3 = str1.concat(str2);
        System.out.println("str1 = "+ str1);
        System.out.println("str3 = "+str3);
        System.out.println("str3.contains(str1) ->"+str3.contains(str1));
        // 字符串如果要比较字符序列, 一定要使用equals
        str1 = "qimu";
        str2 = "qimu";
        str3 = new String("qimu");
        System.out.println("str1.equals(str3)->"+str1.equals(str3));
        System.out.println("str3.equals(str2)->"+str3.equals(str2));
        // 不区分大小写的比较
        String str4 = new String("QiMu");
        System.out.println("str4.equals(str1)->"+str4.equals(str1));
        System.out.println("str4.equalsIgnoreCase(str1)->"+str4.equalsIgnoreCase(str1));
        // indexOf, 获取子串在当前串中的索引,如果不存在就返回-1
        str1 = "I like JavaScript and Java!";
        //indexOf(ch)
        System.out.println("str1.indexOf('l')->"+str1.indexOf('l'));
        System.out.println("str1.indexOf(\"Java\")->"+str1.indexOf("Java"));
        System.out.println("str1.indexOf(\"Java\",8)->"+str1.indexOf("Java",8));
        // lastIndexOf()
        System.out.println("str1.lastIndexOf(\"a\")->"+str1.lastIndexOf("a"));
        str2 = str1.replace("Java", "爪哇");
        System.out.println("str1 = " + str1);
        System.out.println("str2 = " + str2);
        String[] s = str1.split(" ");
        System.out.println(Arrays.toString(s));
        str1 = "上海自来水来自海上";
        System.out.println("str1.startsWith(\"上海\") - >" + str1.startsWith("上海"));
        System.out.println("str1.endsWith(\"上海\") - >" + str1.endsWith("海上"));
        str1 = "I like JavaScript and Java!";
        str2 = str1.toLowerCase();
        str3 = str1.toUpperCase();
        System.out.println("str1 = "+str1);
        System.out.println("str2 = "+str2);
        System.out.println("str3 = "+str3);
        str1 = " 哈 哈 哒 ";
        str2 = str1.trim();
        System.out.println("str1.length() ->"+str1.length());
        System.out.println("str2.length() ->"+str2.length());
        // 截取子串
        str1 = "I like JavaScript and Java!";
        str2 = str1.substring(7);
        System.out.println("str2 = " + str2);
        str3 = str1.substring(7,17);// 前闭后开的区间
        System.out.println("str3 = " + str3);
    }
}

StringBuilder

先看说明:

构造方法:

StringBuilder() 
	构造一个没有字符的字符串构建器,初始容量为16个字符。  
StringBuilder(CharSequence seq) 
	构造一个包含与指定的相同字符的字符串构建器 CharSequence 。  
StringBuilder(int capacity) 
	构造一个没有字符的字符串构建器,由 capacity参数指定的初始容量。  
StringBuilder(String str) 
	构造一个初始化为指定字符串内容的字符串构建器。  

常用的创建方式:

StringBuilder sb = new StringBuilder("这是一个可变的字符串");

StringBuilder是一个对象,不能使用“+”连接,不能使用 = ""直接赋值。

常用的API: (字符串拥有的API,这兄弟基本都有)

StringBuilder append(xxxx xx)
	在当前的字符序列中追加新的字符。
StringBuilder delete(int start, int end) 
	删除此序列的子字符串中的字符。  
StringBuilder deleteCharAt(int index) 
	删除 char在这个序列中的指定位置。  
StringBuilder insert(int index, Xxxx xx)
	在index位置插入xx
StringBuilder reverse() 
	导致该字符序列被序列的相反代替。  
void setCharAt(int index, char ch) 
	指定索引处的字符设置为 ch 。  
ing toString() 
	返回表示此顺序中的数据的字符串。  

程序案例:

public class StringBuilderTest {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder("这是一个可变的字符串");
        StringBuilder sb1 = sb.append(",可变就是可以改变");
        System.out.println(sb);
        System.out.println(sb == sb1);
        sb1.deleteCharAt(5);
        System.out.println(sb);
        sb1.delete(5,8);
        System.out.println(sb);
        sb.insert(5,"非常好");
        System.out.println(sb);
        sb.append("[上海自来水来自海上]");
        System.out.println(sb);
        sb.reverse();
        System.out.println(sb);
    }
}

之前循环拼接字符串的案例,就应该使用StringBuilder

public class StringBuilderTest1 {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0;i < 10000;i++){
            sb.append(i);
        }
        System.out.println(sb);
    }
}

我们也可以使用StringBuffer。StringBuffer的API和StringBuilder完全一致。

  • 当我们创建的可变字符序列是局部变量的时候统一使用StringBuilder。

  • 当我创建的字符可变字符序列是成员变量,并且当前的程序可能会出现高并发情况,建议使用StringBuffer。

异常

 

异常:程序运行过程中的非正常现象。

java中程序表现的非正常现象有两种:Error(错误)和Exception(异常)

Error:重大错误,通过程序无法更正等等。

Exception:由于操作不当或者其他原因导致的程序非正常显现,可以通过捕获处理的情况。

java中关于异常的一类:

当java程序出现异常的时候,JVM会将这个异常信息,封装在一个对象里面,然后将这个对象直接“丢出来”。凡事封装异常信息的这类对象就是Excpetion类的实例对象。

为了区分各种不同的错误信息,就出现了各种不同的Exception。

看看之前见过的一些异常信息:

异常处理

为什么要处理异常

 public class ExceptionTest2 {
     public static void main(String[] args) {
         System.out.println("程序运行开始..");
         int x = 10 / 0;// 这里会有一个数学运算异常
         System.out.println("程序运行结束..");
    }
 }

上面的程序会被数学运算异常中断。我们希望在程序运行过程中,如果出现异常那么程序可以按照我们预定的方式处理异常,然后继续运行,而不是中断。

我们在没有处理这个异常的情况,这个异常会被直接抛到运行环境(JVM)。JVM会直接中断程序,处理异常的思路就是不要将异常抛给运行环境。

java中提供了两种异常处理的方案:捕获和申明抛出。

我们处理异常的目的是为了让程序在出现异常之后可以继续正常运行。

使用try..catch捕获异常

java中的一种捕获异常的语法:

 try{
    // 可能会出现异常的代码
 }catch(异常类型 异常对象){
    // 异常的处理代码
 }

案例:

 public class ExceptionTest2 {
     public static void main(String[] args) {
         System.out.println("程序运行开始..");
         try {
             int x = 10 / 0;// 这里会有一个数学运算异常
        }catch (ArithmeticException e){// e就是封装了异常信息的对象
             System.out.println("出现了运算错误:"+e.getMessage());
             // 记录,给用户友好的提示。
        }
         System.out.println("程序运行结束..");
    }
 }

try...catch的使用问题

  • catch捕获的异常类型必须符合try中出现的异常类型,否则无法捕获。

 public class ExceptionTest3 {
     public static void main(String[] args) {
         Scanner sc = new Scanner(System.in);
         System.out.println("请输入一个整数:");
         int x = -1;
         try {
             x = sc.nextInt();// 这里可能出现异常(全限定类名) java.util.InputMismatchException
        }catch (NullPointerException e){
             System.out.println("这里出现了空指针异常:"+e.getMessage());
        }
         System.out.println("您输入的数字是:" + x);
    }
 }

上面的案例中,try中抛出的异常和catch中捕获的异常类型是不匹配的,所以无法捕获这个异常。

  • 在try语句块中一旦出现异常,直接进入catch块,出现异常的代码后的程序就不会再运行。

 public class ExceptionTest4 {
     public static void main(String[] args) {
         Scanner sc = new Scanner(System.in);
         System.out.println("请输入一个整数:");
         int x = -1;
         try {
             x = sc.nextInt();// 这里可能出现异常(全限定类名) java.util.InputMismatchException
             System.out.println("输入完成");
        }catch (InputMismatchException e){
             System.out.println("这里出现了空指针异常:"+e.getMessage());
        }
         System.out.println("您输入的数字是:" + x);
    }
 }

上面的程序中,一旦"x = sc.nextInt()"出现异常,那么后面的输出语句就不会再执行。

多catch块的情况

有的时候,我们会遇到这样的情况:在try块中可能会有各种不同的异常出现。

我们可以使用多个catch块捕获异常。

 public class ExceptionTest5 {
     public static void main(String[] args) {
         System.out.println("程序运行开始--");
         try{
             if(Math.random() > 0.5){
                 int x = 10 / 0; //数学运算异常
            }
             if(Math.random() > 0.5){
                 String str = null;
                 str.toLowerCase();// 空指针异常
            }
         // 利用多个catch捕获不同的异常信息
        }catch (ArithmeticException e){
             System.out.println("这里出现了数学运算异常:"+e.getMessage());
        }catch(NullPointerException e){
             System.out.println("这里出现了空指针异常:"+e.getMessage());
        }
         System.out.println("程序运行结束--");
    }
 }

当我们使用多个catch块的时候,一定要注意,子类型的异常捕获必须在父类型异常捕获之前:

因为如果父类在前面会直接捕获所有的子类异常,后面的子类异常的捕获就没有意义了。

所以一定要将子类写在前面,父类写在后面。

使用throws申明异常

thrrows和try..catch完全不同的是,不处理异常,只是申明抛出。有调用者处理。

语法:

 public void method() throws XXXXXException{
     // 这里是可能出现异常的代码
 }

意思就是:当我们在方法的签名的后面使用throws申明了这个方法抛出的异常之后,这个方法内部就不需要使用try..catch处理异常。 由这个方法的调用者处理:

 public class ExceptionTest7 {
     public static void main(String[] args) {
         try {
             method();// 调用者处理异常
        } catch (ArithmeticException e) {
             e.printStackTrace();
        } catch (NullPointerException e) {
             e.printStackTrace();
        }
    }
     // 申明了两个异常,方法中可以不处理
     public  static void method() throws ArithmeticException,NullPointerException{
         // 运行时异常,了理论上上不需要申明的。
         System.out.println("程序运行开始--");
             if(Math.random() > 0.5){
                 int x = 10 / 0; //数学运算异常
            }
             if(Math.random() > 0.5){
                 String str = null;
                 str.toLowerCase();// 空指针异常
            }
         System.out.println("程序运行结束--");
    }
 }

method申明了异常,method这个方法就好比之前的try语句块。异常的处理是交给调用者的。

finally语句块

我们知道的语法:

 try{
    可能有异常的代码
 }catch(异常类型 异常对象){
    异常处理的程序
 }finally{
    // 最终一定要执行的程序
 }

我们有时会遇到一些情况,无论是否出现异常,有些程序必须执行。最常见的就是资源释放。

看一个例子:

 try{
     ①连接数据库,产生一个连接
     ②创建一个引擎
     ③执行查询操作
     ④关闭连接(一定要关闭)
 }catch(Exception e){
    .....
 }

上面的程序逻辑中,如果第②或者③步骤出现异常,就直接进入catch块,不再执行④。这时连接就无法关闭了。

为了解决上面的问题,就有finally语句块,finally必须执行。无论是否有异常。

常見的结构:

 try{
    ①连接数据库,产生一个连接
    ②创建一个引擎
    ③执行查询操作
 }catch(Exception e){
    .....
 }finally{
 
  ④关闭连接(一定要关闭)
 
 }

看案例测试:

 public class ExceptionTest8 {
     public static void main(String[] args) {
         try {
             if(Math.random() > 0.5){
                 int x = 10 / 0; //数学运算异常
            }
             System.out.println("--数学运算正常--");
             if(Math.random() > 0.5){
                 String str = null;
                 str.toLowerCase();// 空指针异常
            }
             System.out.println("--字符串转换正常--");
        } catch (ArithmeticException e) {
             System.out.println("数学运算异常:"+e.getMessage());
        } catch (NullPointerException e) {
             System.out.println("空指针异常:"+e.getMessage());
        }finally {
             System.out.println("finally语句块");
        }
    }
 
 }

在任何地方写return语句,finally都会在return之前执行。

 public static String checkStr(){
     try {
         if(Math.random() > 0.5){
             String str = null;
             str.toLowerCase();
        }
         System.out.println("没有异常");
         return "没有异常";
    } catch (NullPointerException e) {
         System.out.println("空指针异常");
         return "空指针异常";
    }finally {
         System.out.println("finally语句块");
    }
 }

集中组合:

  • try,catch,finally语句块都不能单独出现。

  • 常见组合:

    • try.....catch...

    • try...catch...finally

    • try...finally

try....finally的情况:

 // 从数据库查询所有用户   sql异常直接申明,不做处理
 public List<User> queryAll()throws SQLException{
     try{
         获取连接
         创建引擎
         查询数据
         返回数据
    }finally{
         关闭连接
    }
 }

一个简单的笔试题:

 public class Demo1 {
     public static void main(String[] args) {
         int x = 0 ;
         method(x);
    }
     public static void method(int x){
         System.out.println("A");
         try {
             int z = 10 / x;
             try{
                 System.out.println("B");
            }finally {
                 System.out.println("C");
            }
        } catch (Exception e) {
             try{
                 System.out.println("D");
                 int [] xs = {};
                 xs[100] = 0;
            }catch (NullPointerException eX){
                 System.out.println("F");
            }/*catch (IndexOutOfBoundsException ex){
                 System.out.println("S");
             }*/
        }finally {
             System.out.println("E");
        }
    }
 }

常见的运行时异常

[1] 空指针异常

当我们视图调用一个为null的对象的方法或者属性的时候,就出现了空指针异常。

对象还没有被创建,就视图使用这个对象了。

 public static void main(String[] args) {
     Person p = null;
     p.showInfo();// 空指针
 }

检查这个异常的方式,就是在出现异常的哪一行。把这一行的对象逐个检查。找对象的技巧就是所有的"."前面的家伙。

上面的“.”前面是: p对象。 p.getObject()方法的返回值。

[2]类型转换异常

视图将一个并非当前类型的实例对象强制转换为当前类对象。

 public class ExceptionTest11 {
     public static void main(String[] args) {
         Object o = new Object();
         //Person p = (Person) o;// ClassCastException
         Person p1 = new Student();
         Player p2 = (Player) p1;// ClassCastException
    }
 }
 
 class Person11{ }
 class Student extends Person{}
 class Player extends Person{}

[3]索引越界

 public class ExceptionTest12 {
     public static void main(String[] args) {
         int [] arr = {1,2,3};
         //arr[3] = 10;// 数组索引越界
         String str = "abcdefg";
         char cs = str.charAt(-1);// 字符串索引越界
    }
 }

[4]非法参数异常

 // 设置一个负数容量
 ArrayList al = new ArrayList(-10);

[5]格式转换异常

 // 字符串转换为数字
 String str = "123abc";
 int x = Integer.parseInt(str);

日期相关的常用类

java.util.Date

创建系统时间对象。

构造方法:

看代码:

 public class DateTest {
     public static void main(String[] args) {
         // 创建一个当前系统时间对象
         Date date = new Date();
         System.out.println(date);// Fri Feb 25 13:38:54 CST 2022
 
         Date d1 = new Date(6549876545665L);
         System.out.println(d1);// Wed Jul 23 02:09:05 CST 2177
    }
 }

常用的方法:

 boolean after(Date when) 
  测试此日期是否在指定日期之后。  
 boolean before(Date when)
  测试此日期是否在指定日期之前。  
 Object clone()
  返回此对象的副本。  
 int compareTo(Date anotherDate)
  比较两个日期进行订购。  
 boolean equals(Object obj)
  比较两个日期来平等。  
 static Date from(Instant instant)
  从 Instant对象获取一个 Date的实例。  
 long getTime()
  返回自1970年1月1日以来,由此 Date对象表示的00:00:00 GMT的毫秒 数 。
 void setTime(long time)
  设置此 Date对象以表示1970年1月1日00:00:00 GMT后的 time毫秒的时间点。  

 public class DateTest {
     public static void main(String[] args) {         // 创建一个当前系统时间对象
         Date date = new Date();
         System.out.println(date);// Fri Feb 25 13:38:54 CST 2022
         Date d1 = new Date(6549876545665L);
         System.out.println(d1);// Wed Jul 23 02:09:05 CST 2177
         Date d2 = new Date(12345678945678L);
         Date d3 = new Date(12345678945680L);
         boolean result = d3.after(d2);
         System.out.println("d3.after(d2) ->"+result);
         System.out.println("d3.compareTo(d2)->"+d3.compareTo(d2));
 
         d2.setTime(12345678945681L);
         System.out.println("d3.after(d2) ->"+d3.after(d2));
         System.out.println("d3.compareTo(d2)->"+d3.compareTo(d2));
         System.out.println(d2.getTime());

         // 获取当前系统时间的毫秒数
         long time = System.currentTimeMillis();
         ArrayList as = new ArrayList();
         for (int i = 0;i< 1000000;i++){
             as.add(0,i);
        }
         long time1 = System.currentTimeMillis();
         System.out.println(":" + (time1-time));
    }
 }

Date输出的格式不是我们常见,我们需要格式化日期为我们看得懂。

java提供了专门格式化日期的类:java.text.DateFormat.

这个类中有方法:

 String format(Date date) 
 将日期格式化成日期/时间字符串。  

用来格式化日期,但是这个类是抽象类。我们可以使用它的子类java.text.SimpleDateFormat

用法:

 public class DataFormatTest {
     public static void main(String[] args) {
         Date date = new Date();
         // 创建SimpleDateFormat 指定格式日期的模版
         SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
         // 格式化日期
         String str = dateFormat.format(date);
         System.out.println(str);// 2022-02-25 13:55:54
    }
 }

java.util.Calendar

Calendar是抽象类,不能直接创建类对象,建议使用其子类GregorianCalendar。当然我们也可以通过Calendar提供的静态方法获取一个Calendar对象。

 static Calendar getInstance() 
  使用默认时区和区域设置获取日历。  

案例:

 public class CalendarTest {
     public static void main(String[] args) {
         // 使用子类创建对象
         Calendar d1 = new GregorianCalendar();
         System.out.println(d1);
         // 调用静态方法获取对象
         Calendar d2 = Calendar.getInstance();
         System.out.println(d2);
    }
 }

Calendar提供了一个get方法可以获取日期的任何一个部分:

 public class CalendarTest {
     public static void main(String[] args) {
         Calendar d1 = new GregorianCalendar();
         System.out.println(d1);
         // 调用静态方法获取对象
         Calendar d2 = Calendar.getInstance();
         System.out.println(d2);
         // 取出日期的每个部分
         System.out.println(d2.get(Calendar.YEAR) + "年");
         System.out.println(d2.get(Calendar.MONTH) + "月");
         System.out.println(d2.get(Calendar.DATE) + "日");
         System.out.println(d2.get(Calendar.HOUR_OF_DAY)+":"+d2.get(Calendar.MINUTE)+":"+d2.get(Calendar.SECOND));
    }
 }

Calendar还提供了几个好用的API

 void set(int field, int value) 
  将给定的日历字段设置为给定的值。  
 void set(int year, int month, int date)
  设置日历字段中的值 YEAR , MONTH和 DAY_OF_MONTH 。  
 void set(int year, int month, int date, int hourOfDay, int minute)
  设置日历字段中的值 YEAR , MONTH , DAY_OF_MONTH , HOUR_OF_DAY和 MINUTE 。  
 void set(int year, int month, int date, int hourOfDay, int minute, int second)
  设置字段中的值 YEAR , MONTH , DAY_OF_MONTH , HOUR_OF_DAY , MINUTE和 SECOND 。  
 Calendar d3 = Calendar.getInstance();
 d3.set(Calendar.DATE,26);
 d3.set(2025,2,5);
 //.....

Calendar和Date之间的转换。

 // 将Calendar转换为Date
 Date date = d3.getTime();
 System.out.println(date);
 // 将Date转换Calendar
 Calendar d4 = Calendar.getInstance();
 d4.setTime(date);

Object类

Object类是java中所有的类的老祖宗。任何java类在没有继承任何其他类的情况下,默认就继承了Object。

Object中的方法:

tips:Object中的方法,每个对象都拥有。

[1]hashCode方法

 int hashCode() 
  返回对象的哈希码值。   返回一个数字,唯一表示这个对象。
 public class ObjectTest {
     public static void main(String[] args) {
         Person p = new Person();
         System.out.println(p.hashCode());//460141958
         p = new Person();
         System.out.println(p.hashCode());//1163157884
    }
 }

很多时候我们可能会自己重写Object中定义的HashCode。

 public class Person {
     // 重写父类的HashCode
     @Override
     public int hashCode() {
         return 1;
    }
 }

[2]equals()

 boolean equals(Object obj) 
  指示一些其他对象是否等于此。  

修改Person

 public class Person {
     private String name;
     private int age;
 }
 Person p1 = new Person("大锤",15);
 Person p2 = new Person("大锤",18);
 System.out.println(p1 == p2);// false
 System.out.println(p1.equals(p2));// false

我们使用equals比较p1和p2的时候依然是false。

查看Object中的equals源码:

p1.equals(p2); --> p1 == p2

String类就重写了equals方法,比较字符序列,我们也可以重写equals方法。

equals方法重写:

     public boolean equals(Object obj){
         if(this == obj)// 如果地址相同,直接返回true
             return true;
         if(obj == null)// obj为null返回false
             return false;
         if(!(obj instanceof Person)) // obj不是Person,返回false;
             return false;
         // 强制类型转化
         Person p = (Person) obj;
         //if(this.name == p.getName())
 //       if(this.name.equals(p.getName()) && this.getAge() == p.getAge()){
 //           return true;
 //       }
 //       return false;
         // 三目运算
         return this.name.equals(p.getName())?this.age==p.getAge():false;
    }

[3]toString();

 String toString() 
  返回对象的字符串表示形式。  

在没有重写的情况下:

 System.out.println(p1);

Object中的toString源码:

getClass().getName() 获取全限定类名。

我们可以重写:

 @Override
 public String toString() {
     return "Person{" +
             "name='" + name + '\'' +
             ", age=" + age +
             '}';
}