21.List集合的5种遍历方式:
List<String> list = new ArrayList<>() ;
list.add("hello") ;
list.add("JavaEE") ;
list.add("i'm coming") ;
//方式1:
Object[] objects = list.toArray() ;
for(int x = 0 ; x < objects.length; x ++){
String s = (String)objects[x] ;
System.out.println(s) ;
}
//方式2:Collection集合的迭代器
Iterator<String> it = list.iterator() ;
while(it.hasNext()){
String s = it.next() ;
System.out.println(s) ;
}
//方式3:使用List集合的特有功能
for(int x = 0 ; x< list.size() ; x++){
String s = list.get(x) ;
System.out.println(s) ;
}
//方式4:使用列表ListIterator遍历List集合
ListIterator<String> lit = list.listIterator() ;
while(lit.hasNext()){
String s = lit.next() ;
System.out.println(s) ;
}
//方式5:jdk5以后 提供一个增强for循环,替代迭代器
for(String s:list){
System.out.println(s) ;
}
22.List集合特点和Set集合有什么区别
List集合保证元素有序(存储和取出一致),Set集合无序的:不能保证迭代顺序恒久不变
List集合可以让元素重复/set集合本身就是保证元素唯一!
子实现类的分别的应用场景:
ArrayList
Vector
LinkedList
HashSet
TreeSet
23.List集合三个子实现类的特点:
ArrayList:
底层数据结构是数组,数组的特点:查询快,增删慢
查询的:通过整数索引,立即查询到元素
从线程角度考虑:由于实现是不同步的,执行效率高,线程不安全!
默认容量大小:10个
扩容机制:1.5倍方式扩容
Vector:
底层数据结构是数组,查询快,增删慢
线程角度:实现同步的,里面的成员方法大部分都会有synchronized同步锁,保证安全性
安全性很高,但是执行效率低,单线程程序中,使用ArrayList替代Vector
LinkedList:
底层数据结构是一个链表,
线程角度: 不同步的,单线程中执行效率高,安全性低!
链表:数据域和指针域组成!
当一些需求没有明确提示使用什么类型的时候,都是用ArrayList(单线程程序中)
24.ArrayList集合
构造方法:
public ArrayList():构造一个初始容量为十的空列表。
public ArrayList(int initialCapacity):指定容量大小
25.Vector集合
在List集合中最明显的特点:线程安全
目前为止:线程安全的类(多线程中使用)
StringBuffer/Vector/
线程安全的方法:Collections:针对集合操作工具类
public static <T> List<T> synchronizedList(List<T> list):保证列表同步(安全方法)
特有功能:
public void addElement(Object obj):添加元素
public boolean removeElement(Object obj):直接从Vector集合中删除指定的元素
public Object elementAt(int index):通过指定的索引值获取元素------类似于List集合中 get(int index)
public Enumeration<Object> elements():(特有迭代器)获取Vector集合中的枚举组件接口 -----类似于 public Iterator iterator():迭代器
boolean hasMoreElements() ---->类似于 boolean hasNext() 判断是下一个遍历的元素
Object nextElement() ---类似于 Object next() 获取下一个元素
应用场景:
如果环境是多线程环境,而且使用集合存储数据,保证线程安全,Vector集合!
Vector集合遍历:7种 使用特有功能: public Object elementAt(int index) +size()
for(int x = 0 ; x < v.size() ; x ++){
String s = v.elementAt(x);
System.out.println(s) ;
26.LinkedList集合
不同步,线程不安全,执行效率高; 数据结构是:链表
特有功能:
public void addFirst(Object e):在链表开头插入元素
public void addLast(Object e):将元素追加到链表的末尾
public Object getFirst():获取链表的第一个元素
public Object getLast():获取链表的最后一个元素
public Object removeFirst():删除链表第一个元素并获取第一个元素
public Object removeLast():删除链表最后一个元素并获取
27.List集合嵌套应用:ArrayList<ArrayList
<Student>> :如何嵌套使用?
分析:
0)创建出大的集合
1)三个小集合:ArrayList<Student>,添加几个学生数据
2)分别将三个小集合都添加到集合中
3)遍历大集合中
遍历小集合,分别获取信息
举例: 有3个Java基础班,每一个班级都看成一个ArrayList<Student>,
这个三个班级组成了一个大的集合ArrayList<ArrayList<Student>>
获取出每一个班级的学生的姓名和年龄;
public class ArrayListTest {
public static void main(String[] args) {
//创建出大的集合
ArrayList<ArrayList<Student>> bigArray = new ArrayList<>() ;
//第一个Java基础班
ArrayList<Student> firArray = new ArrayList<>() ;
//创建3个学生
Student s1 = new Student("唐僧",50) ;
Student s2 = new Student("孙悟空",44) ;
Student s3 = new Student("猪八戒",40) ;
//添加数据
firArray.add(s1) ;
firArray.add(s2) ;
firArray.add(s3) ;
//添加到大集合中
bigArray.add(firArray) ;
//第二个Java基础班
ArrayList<Student> secArray = new ArrayList<>() ;
//三个学生
Student s4 = new Student("宋江",40) ;
Student s5 = new Student("武松",35) ;
Student s6 = new Student("鲁智深",32) ;
secArray.add(s4) ;
secArray.add(s5) ;
secArray.add(s6) ;
bigArray.add(secArray) ;
//第三个Java基础班
ArrayList<Student> thirdArray = new ArrayList<>() ;
Student s7 = new Student("曹操",45) ;
Student s8 = new Student("刘备",36) ;
Student s9 = new Student("周瑜",33) ;
thirdArray.add(s7) ;
thirdArray.add(s8) ;
thirdArray.add(s9) ;
bigArray.add(thirdArray) ;
//遍历 ArrayList<ArrayList<Student>>
for(ArrayList<Student> array :bigArray){
for(Student s :array ){
System.out.println(s.getName()+"\t"+s.getAge());
}
}
}
}
public class Student {
private String name ;
private int age ;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = 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;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
28.Set集合
1.1无序.保证元素唯一!
代表HashSet子实现类
底层一个HashMap的实例,保证元素唯一(底层哈希表,和HashMap有关系),不能保证迭代顺序恒久不变!
1.2面试题::为什么HashSet<String>,添加重复的String数据,能够将元素唯一!
hashSet集合的添加功能,add方法:间接依赖于的HashMap的put方法----->底层依赖于 hashCode()和equals()方法
首先,要比较当前存储String类型数据的哈希码值是否相同,如果相同
比较是内容是否一样,调用equals()方法,String类型已经重写了Object,比较内容!
29.TreeSet集合
TreeSet<E>:底层依赖于TreeMap -----Red-Black-Tree:数据结构:红黑树(自平衡的二叉树结构) 有两种排序:自然排序/比较器排序,取决于你的构造方法!
TreeSet<Integer>(无参构造方法创建TreeSet集合对象) ---->Integer本身实现Comparable接口:compareTo方法,
TreeSet<Student>:通过无参构造方法创建对象----前提条件:给定排序条件,如果当前Student类没有实现Comparable接口,类转换异常;
解决:
自定义对象所在的类必须实现Comparable接口的compareTo()方法,实现自己的排序规则!
举例:,键盘录入5个学生的姓名,语文成绩,英语/数学,总分从高到低排序(自然排序)
public class TreeSetTest {
public static void main(String[] args) {
//创建TreeSet集合:无参构造方法:自然排序
TreeSet<Student> ts = new TreeSet<>() ;
System.out.println("录入学生信息开始: ");
//录入5个学生
for(int x = 1 ; x <=5 ; x ++){
//创建键盘录入对象
Scanner sc = new Scanner(System.in) ;
//提示并录入数据
System.out.println("请你输入第"+x+"个学生的姓名: ") ;
String name = sc.nextLine() ;
System.out.println("请你输入第"+x+"个学生的语文成绩: ") ;
String chineseStr = sc.nextLine() ;
System.out.println("请你输入第"+x+"个学生的数学成绩: ") ;
String mathStr = sc.nextLine() ;
System.out.println("请你输入第"+x+"个学生的英语成绩: ") ;
String englishStr = sc.nextLine() ;
//创建学生对象:封装学生数据
Student s = new Student() ;
s.setName(name) ;
s.setChinese(Integer.parseInt(chineseStr)) ;
s.setMath(Integer.parseInt(mathStr)) ;
s.setEnglish(Integer.parseInt(englishStr)) ;
//将学生添加集合中
ts.add(s) ;
}
System.out.println("录入学生信息完毕: ") ;
System.out.println("学生的总分从高到低排序如下: ") ;
System.out.println("姓名\t语文成绩\t数学成绩\t英语成绩");
//遍历集合
for(Student s:ts){
System.out.println(s.getName()+"\t"+s.getChinese()+"\t"+s.getMath()+"\t"+s.getEnglish());
}
}
}
public class Student implements Comparable<Student>{
//姓名
private String name ;
//语文成绩
private int chinese ;
//数学成绩
private int math ;
//英文成绩
private int english ;
public Student() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getChinese() {
return chinese;
}
public void setChinese(int chinese) {
this.chinese = chinese;
}
public int getMath() {
return math;
}
public void setMath(int math) {
this.math = math;
}
public int getEnglish() {
return english;
}
public void setEnglish(int english) {
this.english = english;
}
//计算总分
public int getSum(){
return chinese + math + english ;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", chinese=" + chinese +
", math=" + math +
", english=" + english +
'}';
}
//要进行比较
@Override
public int compareTo(Student s) {
//排序的主要条件:
//总分从高到低进行排序
int num = s.getSum() - this.getSum() ;
//分析次要条件:
//总分相同,按照语文成绩:从低到高
int num2 = (num==0)?(this.chinese-s.chinese):num ;
//总分相同,语文相同,按照数学成绩:从低到高
int num3 = (num2==0)?(this.math-s.math):num2;
//总分相同,语文相同,按照英语排
int num4 = (num3 ==0)?(this.english-s.english):num3 ;
//如果都相同,不一定是同一个人,按照学生的姓名进行比较(字典顺序比较)
int num5 = (num4==0)?(this.name.compareTo(s.name)):num4 ;
return num5;
}
}
30.HashSet存储自定义对象如何保证元素唯一
HashSet集合的添加功能,add方法底层依赖HashMap的put方法---->hashCode()和equals方法
存储的自定义对象所在的类必须重写Object的hashCode()和equals方法,保证元素唯一!
31.获取一个类的字节码文件对象有几种方式
Object类的getClass()----->Class (class 包名.类名)
任意Java类型的class属性
package com.qf;
class Demo{}
Class c = Demo.class;
32.Map集合
Map接口---->不能实例化
具体的子类
HashMap<K,V>
TreemMap<K,V>
基本功能:
V put(K key,V value)添加键值对元素,返回键对应的值
void clear() 暴力删除,将Map清空
V remove(Object key):删除指定的键,返回被删除的键对应的值
是否包含指定的键
boolean containsKey(Object key)
是否包含指定的值
boolean containsValue(Object value)
int size():获取Map的键值对个数(元素)
Collection<V> values():获取Map中所有的值
Set<K> keySet():获取所有的键的集合
V get(Object key):通过键获取值
Set<Map.Entry<K,V>> entrySet():获取Map集合中的键值对对象.
Map集合的遍历方式有 两种:
方式1:(通用)
Set<K> keySet():获取所有的键的集合------>
V get(Object key):通过键获取值---------->
方式2:
Set<Map.Entry<K,V>> entrySet():获取Map集合中的键值对对象 ------>
K getKey() 键值对对象获取键---->
V getValue() 键值对对对象获取值--->
33.HashMap<K,V>集合
K键,V值----存储的一系列的键值对元素.
Map集合特点:不能包含重复的键(必须唯一),如果重复,后面的值会将前面的值覆盖
如果HashMap存储的键为自定义类型,
那么当前自定义类型必须重写equals()和hashCode(),保证键唯一,因为Map集合针对键有效,跟值无关
TreeMap<K,V>:针对元素(键)自然排序或者比较器排序,取决于构造方法
public TreeMap():自然排序:
public TreeMap(Comparator<? super K> comparator):比较器排序
34.面试题:Map和Collection的区别?
1)存储结构不一样
Collection<E>,单例集合,只能存储一种引用数据类型----->理解为"光棍"
Map<K,V>,双列集合,可以存储多个引用数据类型---->理解为"夫妻对"
2)遍历方式不同的
Collection:迭代器遍历---最终使用的增强for
Map<K,V>: 通用的方式:将所有K获取到,通过键找值!
3)有一定的关系
Collection里面的Set的子实现类都依赖于Map实现
35.Collections类:针对集合操作的工具类:
特有功能:
public static void reverse(List<?> list):将List元素反转
public static void shuffle(List<?> list):将List集合随机置 public static <T extends Comparable<? super T>> void sort(List<T> list):针对List集合自然排序
public static <T> void sort(List<T> list,Comparator<? super T> c):针对List集合进行比较器排序
public static <T> int binarySearch(List<? extends Comparable<? super T>> list,T key)
在集合中查询key元素第一次出现索引值 (集合有序!----List<Integer>
获取集合中最大值和最小值元素
//public static <T extends Object & Comparable<? super T>> T min(Collection<? extends T> coll)
public static <T> List<T> synchronizedList(List<T> list) :和多线程结合使用!
举例:使用集合模拟斗地主的洗牌和发牌,发到每个人手上牌有序
分析:
1)创建牌盒
HashMap<Integer,String>存储的编号和对应的牌
ArrayList<Integer>单独存储编号
2)
int index = 0 ;
装牌:点数,花色
点数数组: "3","4"....,"10","J","Q","A","2"
花色---拼接点数
index ++ ;
单独添加map集合添加(index,"小王") ;
arrayList集合.add(index) ;
index ++ ;
单独添加map集合添加(index,"大王") ;
3)洗牌:洗的是 ArrayList<Integer>存储编号
4)发牌:发的是编号
三个玩家/底牌 TreeSet<Integer>
发牌的业务算法逻辑
for(int x = 0 ; x < arrry.size() ; x ++){
//判断
//底牌:三张:如果当前x>=array.size()-3
if(x>=arrry.size()-3){
//底牌的:索引值
//通过diPai获取在大的集合获取添加
diPai.add(arrry.get(x)) ;
//三种情况:
}else if(x % 3 == 0 ){
//情况1:----玩家1
plary1.add(arrry.get(x)) ;
}else if(x % 3 == 1){
//玩家2
plary2.add(arrry.get(x)) ;
}else if(x % 3 == 2){
//玩家3
plary3.add(arrry.get(x)) ;
}
//x % 3==3 :发完了
}
5)看牌:
public static void lookPoker(String name,TreeSet<Integer> ts,HashMap<Integer,String> hm)
代码实现:
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.TreeSet;
public class PokerDemo {
public static void main(String[] args) {
//创建牌盒 使用HashMap集合<Integer,String>,Key:它的编号 Value:存储的牌
HashMap<Integer,String> hm = new HashMap<>() ;
//创建ArrayList<Integer>,单独存储的牌的编号
ArrayList<Integer> array = new ArrayList<>() ;
//装牌
//定义花色数组
String[] colors = {"♥","♠","♣","♦"} ;
//定义点数数组
String[] numbers = {"3","4","5","6","7","8","9","10","J","Q","K","A","2"} ;
//定义编号变量index:从0开始
int index = 0 ;
//拼接
for(String number :numbers){
for(String color:colors){
String poker = color.concat(number);
//将编号和进行添加
hm.put(index,poker) ;
array.add(index) ;
//编号++
index ++;
}
}
//添加小王/和大王
hm.put(index,"小王") ;
array.add(index) ;
index ++ ;
hm.put(index,"大王") ;
array.add(index) ;
//System.out.println(array) ;//编号
//洗牌:洗的是编号
Collections.shuffle(array) ;
// System.out.println(array);
//发牌:(发的是编号)发牌需要保证每个玩家手上的牌有序:使用TreeSet集合进行操作
TreeSet<Integer> player1 = new TreeSet<>() ;
TreeSet<Integer> player2 = new TreeSet<>() ;
TreeSet<Integer> player3 = new TreeSet<>() ;
TreeSet<Integer> diPai = new TreeSet<>() ;
for(int x = 0 ; x < array.size(); x++){
//遍历了ArrayList集合中的所有牌对应的编号
if(x>=array.size()-3){
//底牌
diPai.add(array.get(x)) ;
}else if(x % 3 == 0 ){
player1.add(array.get(x)) ;
}else if(x % 3 == 1){
player2.add(array.get(x)) ;
}else if(x % 3 == 2){
player3.add(array.get(x)) ;
}
}
//看牌:三个都可以看牌,看一下底牌的内容:封装成功能
lookPoker("库里",player1,hm) ;
lookPoker("欧文",player2,hm) ;
lookPoker("哈登",player3,hm) ;
lookPoker("底牌",diPai,hm) ;
}
public static void lookPoker(String name,TreeSet<Integer> ts,HashMap<Integer,String> hm){
//输出人的名称
System.out.print(name+"的牌是: ") ;
//TreeSet集合里面存储的自己的牌的编号
for(Integer key:ts){
//获取到每一个编号
//在HashMap集合中通过编号找值
String poker = hm.get(key);
System.out.print(poker+"\t") ;
}
System.out.println();
}
}
36.Thread线程
1.1何创建多个线程?
1)创建线程---->创建进程---->创建系统资源
但是,Java语言是不能够创建资源的,只有C语言
2)不能直接创建系统资源,Jdk提供了一个类Thread,底层封装的
3)java.lang.Thread:线程类(jvm能够运行应用程序并发的执行多个线程)
A)自定义一个类继承Thread类
B)在自定义的类中重写Thread类的run方法----run方法: 放置的耗时的代码 (怎么调用的? 是线程启动start,通过jvm调用run方法
构造函数:
public Thread():创建线程
成员方法:
public final String getName():获取线程名称
public final void setName(String name):设置线程名称
C)在main线程:用户线程中,创建当前这个的类对象---->线程对象
启动线程:start方法
1.2面试题::Jvm是多线程的吗?
Jvm:java虚拟机是多线程
至少有两条线程
一条是main线程,用户线程
还有一个:垃圾回收线程,需要不断将堆内存中不用的对象进行回收,释放内存空间!
1.3 守护线程
public final void setDaemon(boolean on):标记某个线程是否为守护线程
参数如果为true,就是守护线程,如果线程都是守护线程,则JVM退出
注意事项:如果要设置某个线程为守护线程,必须在在启动线程之前,调用setDaemon(true)
1.4 Thread线程的优先级
常量字段
public static final int MIN_PRIORITY = 1 最小优先级
public static final int NORM_PRIORITY = 5 默认优先级
public static final int MAX_PRIORITY = 10 最大优先级
public final int getPriority():获取优先级
public final int getPriority():设置优先级
1.5 多线程的实现方式2:
1)自定义一个类,实现Runnable接口,
2)重写接口中的run方法:就只有一个run
3)在main用户线程中,创建当前类的对象,
然后创建Thread类对象:将当前的那个类的对象作为参数传递
构造方法
Thread(Runnable target)分配一个新的 Thread对象。
Thread(Runnable target, String name):创建线程类对象,将资源类对象作为参数传递,并且给线程设置名称
举例 电影有三个窗口,总共100张电影票被三个窗口售卖,请使用多线程序的方式实现
public class SellTicketDemo {
public static void main(String[] args) {
//创建资源类对象
SellTicket st = new SellTicket() ;
//创建三个线程,并给线程起名字
//Thread(Runnable target ,String name)
Thread t1 = new Thread(st,"窗口1") ;
Thread t2 = new Thread(st,"窗口2") ;
Thread t3 = new Thread(st,"窗口3") ;
//启动线程
t1.start() ;
t2.start() ;
t3.start() ;
}
}
public class SellTicket implements Runnable{ //谁实现Runnable接口:谁就是资源类!
//100张票
public static int tickets = 100 ;
//创建一把锁
private Object obj = new Object() ;
private Demo demo = new Demo() ;
@Override
public void run() {
//模拟一直有票
//t1,t2,t3
while(true){
/**
* synchronized(锁对象){
* 多条语句对共享数据进行操作
* }
*/
//t1,t2,t3睡眠醒来之后
// synchronized (new Object()){//锁对象:不是同一个锁对象,每个线程new Object()用的自己的锁
synchronized (demo){//t1,t2,t3
//判断当前票
if(tickets>0){
//让线程睡眠100毫秒
try {
//t1
//可能出现问题
Thread.sleep(200); //参数为毫秒数
} catch (InterruptedException e) {
e.printStackTrace(); //jvm就会将异常信息打印在控制台上
}
System.out.println(Thread.currentThread().getName()+"正在出售第"+(tickets--)+"张票");
}
}
}
}
}
//自定义的类
class Demo{}
1.6 多线程安全问题的校验标准:
1)检查你的程序是否为多线程环境 是
2)是否存在共享数据: 存在
3)是否有多条语句对共享数据进行操作 也存在
如何解决呢?
现在环境就需要使用多线程实现,1)不能更改
必须存在共享数据,因为三个线程共享一个内存区域, 2)标准不能被更改
Java提供了:使用同步代码块 将 多条对共享数据的操作包裹起来,需要 更改3)
synchronized(锁对象){
多条语句对共享数据进行操作
}
锁对象:可以是任意的Java类对象(jdk提供的任意类或者自定义类型)
必须多个线程使用的 是同一个锁对象! 理解为:"火车上上厕所的门开和关!"
1.7什么是同步方法:
如果一个方法中一进来就是同一个同步代码块,那么可以将synchronized定义在方法声明上,同步块不需要了!
格式:
权限修饰符 synchronized 返回值类型 方法名(形式参数列表){
...
}
同步方法的锁对象是谁: this:代表所在的那个类的对象的地址值引用!
如果当前这个方法:是一个静态方法---可以定义静态的同步方法,锁对象是谁呢?
静态跟类相关的---反射有关系 类名.class
同步代码块synchronized(锁对象){多条对共享数据的操作}
前提条件:锁对象必须是同一个锁(同一个对象)
1.8 死锁现象:多个线程在互相抢占资源数据的时候,出现了线程互相等待的情况!
原因:必须保证多个线程 "共享同一个资源对象"------>生成者消费者模式思想操作
举例:模拟生成者和消费者模式
public class ThreadDemo {
public static void main(String[] args) {
//创建一个学生对象
Student s = new Student() ;
//创建生产者资源类对象
SetThread st = new SetThread(s) ;
//创建消费者资源类对象s
GetThread gt = new GetThread(s) ;
//创建线程类对象
Thread t1 = new Thread(st) ;
Thread t2 = new Thread(gt) ;
//启动线程
t1.start() ;
t2.start() ;
}
}
//生产者类
public class SetThread implements Runnable {
//Student s = new Student() ;
private Student s ; //声明学生变量s
public SetThread(Student s){
this.s = s ;
}
//统计变量
private int x = 0 ;
@Override
public void run() {
while(true){
//t1
synchronized (s){
//判断:
//如果当前生产者没有数据,先等待产生数据
if(s.flag){
try {
s.wait(); //wait()方法为什么定义Object类中? 因为锁对象可以是任意的Java类对象 (包括Object)
// //wait()一旦被调用,会立即释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(x % 2 ==0){
s.name ="高圆圆" ;
s.age = 42 ;
}else {
s.name = "李云迪";
s.age = 39;
}
//改变标记:有数据了
//改变标记
s.flag = true ;
//调用通知对方:唤醒对方线程
s.notify() ;
}
x++ ;//原子性操作
}
}
}
//消费者类
public class GetThread implements Runnable {
//声明学生变量s
private Student s ;
public GetThread(Student s) {
this.s = s ;
}
//Student s= new Student() ;
@Override
public void run() {
//使用学生数据
//System.out.println(s.name+"-"+s.age) ;
//不断的去使用数据
while(true){
synchronized (s){
//如果当前消费者存在数据
if(!s.flag){
//等待将产生的数据消费掉
try {
s.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(s.name+"---"+s.age);
//改变标记
s.flag= false ;
//通知生成者线程,产生数据
s.notify() ;
}
}
}
}
学生类
public class Student {
String name ; //姓名
int age ;//年龄
boolean flag ; //默认没有数据,通过这标记信号:表示是否存在数据
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}