JavaSE学习回顾

JAVA学习网 2020-11-30 16:30:04

 


 

对象与类

Java是一种面向对象程序设计(OOP)语言。面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。

程序中的对象大部分来自标准库,我们也可以自定义对象。

类是构造对象的模板或蓝图,类构造对象的过程称为创建类的实例。下面这段代码就演示了如何定义一个标准的类↓可以根据需要添加方法

1 public class Student {
2     private String name;//私有成员变量
3     public Student() {}//无参构造器
4     public Student(String name) {this.name = name;}//全参构造器
5     public String getName() {return name;}//访问器
6     public void setName(String name) {this.name = name;}//更改器
7 }

接下来我们可以使用这个Student类模板来创建一个Student类对

 Student student = new Student("小明");

上面这行代码为Student类创建了一个实例对象。它在内存中开辟了2个空间,一个是new关键字在堆内存中开辟的,用来存放新建的Student()实例对象。另一个

是在栈中开辟的用来存放指向新创建的实例对象的引用。new后面其实是调用了我们定义的Student类中的全参构造器。

现在就可以借助引用来使用对象中的方法↓

System.out.println(student.getName());

上面这行代码使用了类中的getName()方法来获取name的值,并进行打印。

只有声明没有方法体的方法被称为抽象方法,存在抽象方法的类被称为抽象类。

1 public abstract class Dog{
2     public abstract void bray();
3 }

abstract关键字声明为抽象,上面的bray方法只有方法声明没有方法体,被声明为抽象方法。Dog类中含有抽象方法,必须声明为抽象类。


继承

继承的基本思想是,可以基于已有的类来创建新的类。

新类会继承已有类的变量,方法。我们还可以往新类中添加一些新的方法和字段,使新类能够适应新的情况。

例如,现在我们有一个动物类↓包含成员变量(name,age)方法(sayHello());

 1 public class Animal {
 2     private String name;
 3     private int age;
 4 
 5     public Animal() {}
 6 
 7     public Animal(String name, int age) {
 8         this.name = name;
 9         this.age = age;
10     }
11 
12     public void sayHello(){
13         System.out.println(name + ": Hello");
14     }
15 }

现在我们要编写一个新的类,Dog类它包含包含成员变量(name,age)方法(sayHello(),bray());这时候我们就可以使用继承!

Java的继承使用关键字extends,格式如下↓

1 public class Dog extends Animal {
2     public Dog(String name,int age){
3         super(name,age);
4     }
5     public void bray(){
6         System.out.println("汪汪汪");
7     }
8 }

Dog类它继承了Animal类中的成员变量(name,age)方法(sayHello())

我们只要在Dog类的构造方法中使用super关键字调用Animal类的构造方法并把参数传过去就OK了。

我们把继承类称为子类或派生类,把被继承的类称为超类,父类或基类。

接下来说一下方法的覆盖重写。在Animal里我们定义了一个sayHello()方法,这个方法并不适用于Dog类(你可能不知道!狗只能”汪汪汪“。。)

我们需要在Dog类中把sayHello这个方法进行重写↓把方法的功能改写为我们需要的就可以了

1     @Override
2     public void sayHello(){
3         System.out.println("汪汪汪");
4     }

@Override写在方法前面,作用是编译器可以给你验证@Override下面的方法名是否是你父类中所有的,如果没有则报错。可以不写,


 

接口

Java不支持多继承,例如上面的例子中Dog类继承了Animal类,那么就不能在继承其他类了。

接口是解决Java无法实现多继承的手段之一。我们不能继承多个类,但可以实现多个接口。

Java接口是一系列方法的声明,是一些方法特征的集合。

一个接口只包含有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为(功能)。

举例:Dog类中的bray方法,它会打印“汪汪汪”,但不是每种动物都这么叫。如果是猫类,就需要打印“喵喵喵”。

可以看出,bray方法被不同类实现时,需要具有不同的功能。可以使用接口。

接口的使用步骤:创建接口→实现接口→使用接口(创建完接口后,这时接口里只有方法的声明,想要使用接口中的方法还需要对这些方法进行定义)

1.创建接口(使用关键字interface)

1 public interface Bray {
2     public abstract void bray();
3 }

这里就使用interface关键字创建了一个接口Bray。接口中声明了一个bray抽象方法。接口中的方法默认为抽象方法,所以上面的public abstract可以省略。

2.实现接口(使用到了关键字implements)

1 public class Dog implements Bray{
2     @Override
3     public void bray(){
4         System.out.println("汪汪汪");
5     }
6 }

Dog类中重写了接口中的bray抽象方法。注意,重写的方法有方法体,有具体实现,所以不可以加abstract关键字。

如果是其他类如猫类,你实现的时候就打印“喵喵喵”就好了

3.使用

1     public static void main(String[] args) {
2         Dog dog = new Dog();
3         dog.bray();
4     }

Dog类必须实现接口中的所有抽象方法才能进行使用,否则就只能声明为抽象类。

Java8版本新增了接口的默认实现,使用default关键字。同时还有静态默认方法。

接口中的方法声明为默认方法后,如果实现类中没有重写该方法就使用默认方法。

1     public default void sayHello(){
2         System.out.println("Hello");
3     }

这里一个需要注意的地方。当子类继承父类并实现多个接口时,接口中有同名的默认方法,父类中有同名的方法定义时它的语法规则是怎样的? 

1.如果父类有具体定义,就优先使用父类的方法

2.如果父类没有具体定义,需要在子类中重写该方法,或者指定使用哪个接口的方法。

指定接口方法的格式:

1     public void method(){
2         Interface.super.method;
3     }

上面重写的方法指定使用Interface接口中的method方法。如果方法有返回值,

1     public int method(){
2         return Interface.super.method;
3     }

 


 

多态

多态其实就是用父类(接口)的引用来指向子类(实现类)对象。

Animal obj = new Dog();

上面这行代码就使用了多态。它用Animal类的引用指向了Dog类的对象。

使用多态的好处↓

例如我现在手头上有一只猫和一只狗。。。现在我要给一只给我朋友。请教他这个动物的叫声是怎样的

1     public static void main(String[] args) {
2         Animal cat = new Cat();
3         Animal dog = new Dog();
4 
5         friend(dog);//我给了他一个狗
6     }

朋友又不知道我到底给他猫还是给他狗,他只知道我会给他一只动物(猫或狗)。

1     public void friend(Animal obj){
2         obj.bray();
3     }

这里有一个需要注意的地方,因为obj是Animal类型的,所以他只能使用在Animal类中定义的方法,如果bray方法定义在Dog类中,那么obj是无法访问bray方法的

解决方法是进行强制类型转换。将obj强转为Dog类型,才能使用bray()方法.

回到例子中,朋友拿到我给的动物,这时候我朋友还不知道这个动物怎么叫。

他必须先判断这个动物是猫还是狗,判断完这个动物是狗那么他才自动这个动物的叫声是“汪汪汪”。↓

1     public static void friend(Animal obj){
2         if(obj instanceof Dog){
3             ((Dog)obj).bray();
4         }
5     }

判断过程我们使用到了instanceof关键字,它的功能是用于判断前面的对象是否是后面的类,或者其子类、实现类的实例

当判断完obj是Dog类的实例后,将obj强制转换为Dog类型,然后就可以使用bray方法了。

 


 Lambda表达式

 只有一个抽象方法的接口被叫做函数式接口,需要函数式接口的对象时可以提供一个lambda表达式。

上面定义的Bray接口↓只有bray一个抽象方法,是函数式接口,可以使用lambda表达式。

1  public interface Bray {
2      public abstract void bray();
3  }

下面演示一下,通过提供lambda表达式来创建对象(等号右边就是一个lambda表达式)

1 Bray bray = ()->{
2     System.out.println("汪汪汪");
3 };

lambda表达式不是对象,不过它在为一个类型为函数式接口的变量赋值时会生成一个对象。

System.out::println是一个方法引用。它指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法。

1 public class Main {
2     public static void main(String[] args) {
3         Bray bray = Main::method;
4         bray.bray();
5     }
6     public static void method(){
7         System.out.println("汪汪汪");
8     }
9 }

上面这段代码就演示了如何使用方法引用。

Bray bray = Main::method;

方法引用类似于lambda表达式,上面这行代码指示编译器生成一个Bray函数式接口的实例,然后用已经定义的method方法来覆盖bray抽象方法。

 


 

内部类

内部类就是在类中定义的类。

成员内部类

1 public class Dog {//外部类
2     public class Heart{//内部类
3         public void beat(){
4             System.out.println("扑通扑通");
5         }
6     }
7 }

Heart类定义在Dog类内部。Dog类称为外部类,Heart类称为内部类。

使用内部类:直接创建Dog类中的Heart类↓也可以使用Dog类中的方法间接创建Heart类对象进行使用。

1     public static void main(String[] args) {
2         Dog.Heart heart = new Dog().new Heart();
3         heart.beat();
4     }

静态内部类:声明为static,只是为了将类隐藏在另一个类,不需要内部类有外部类的引用。

局部内部类(可以使用方法中的变量

1     public void beat() {
2         class Heart{
3             String sound = "扑通扑通";
4         }
5         System.out.println(new Heart().sound);
6     }

匿名内部类(最常用)

1 public interface Animal {
2     public abstract void beat();
3 }
 1 public class Dog {
 2 
 3     public static void main(String[] args) {
 4         Animal dog = new Animal(){
 5           public void beat(){
 6               System.out.println("扑通扑通");
 7           }
 8         };
 9 
10         dog.beat();
11     }
12 }

使用内部类的好处

1.增强封装,把内部类隐藏在外部类当中,不允许其他类访问这个内部类
2.增加了代码一个维护性
3.内部类可以直接访问外部类当中的成员


 

异常

在Java中,异常对象都是派生于Throwable类的一个类实例。

声明抛出异常

1     public void bray() throws Exception {
2         throw new Exception();
3     }

方法参数后面使用关键字throws声明该方法可能抛出的异常类型

创建一个新的异常类,并使用throw关键字抛出

自定义异常类(定义的类必须派生于Exception类或其子类)

 1 public class MyException extends Exception{
 2     public MyException() {
 3     }
 4 
 5     public MyException(String message) {
 6         super(message);
 7     }
 8 
 9     @Override
10     public String toString() {
11         return "MyException{}";
12     }
13 }

捕获处理异常

1 try{ 
2     throw new Exception();
3 }catch (Exception e){
4     System.out.println(e.getMessage()); 
5 }

try代码块中的代码是可能会抛出异常的代码,如果try代码块抛出的异常与catch指定的异常一样,则执行catch后面的处理异常代码块

代码抛出一个异常时,就会停止处理这个方法中剩余的代码,并退出这个方法。如果这个方法已经获得只有它自己知道的本地资源,这些资源需要进行清理↓

可以使用finally子句解决这个问题。使用finally后不管是否有异常被捕获,在退出try/catch语句块前都会执行finally语句块中的代码

1 InputStream in=System.in;
1 try{
2     try{
3         new Demo().method();
4     }finally{
5         in.close();
6     }
7 }catch(MyException|IOException e){
8     System.out.println(e.toString());
9 }

使用两个嵌套try语句,内层的try语句块确保关闭输入流。外层的try语句块中是处理异常的代码

注意!!!

不要在finally语句块中放入改变控制流的语句(return,throw,break,continue)可能会出现意想不到的结果

1     public int method() {
2         try{
3             throw new Exception();
4         }finally {
5             return 0;
6         }
7     }

我在finally代码块中写入return语句,它直接把我抛出的异常吞掉了!编译器也没检测出问题,所以要小心。。

还有一种方法常用来解决上面的问题,就是带资源的try语句

1         try(InputStream in = System.in;){
2             throw new MyException();
3         } catch (IOException|MyException e) {
4             e.printStackTrace();
5         }

它将开启资源的代码全放在try后面的括号中,在try块退出时会自动调用.close()方法关闭资源。

System.in会抛出IOException异常而我又抛出MyException异常,这两个异常是平级且我希望他们的处理措施是一样的,可以使用上面的写法使用|隔开。


 泛型

泛型程序设计意味着编写的代码可以被不同类型的对象重用。在还没有泛型机制的时候,我们只能使用Object编写适用于多种类型的代码。

使用泛型的好处(使用ArrayList()进行演示)↓

1         ArrayList arrayList = new ArrayList();
2         arrayList.add("aaa");
3         String a = arrayList.get(0);//Error

上面这段代码中,ArrayList没有使用泛型类,所以在获取值时必须使用强制类型转换,把Object类型转换成String类型。

1         ArrayList<String> arrayList = new ArrayList<>();
2         arrayList.add("aaa");
3         String a = arrayList.get(0);

这次我们使用泛型ArrayList类,指定这个数组列表只能存储String对象,获取值时就不需要使用强制类型转换。

类型参数(即尖括号中的东东)只能是引用类型,想存储int这些基本类型必须使用它们的包装类Integer。

下面我定义了一个泛型类,引入一个类型变量E。(通常使用E表示集合的元素类型。K和V表示表的键和值。U,T,S表示任意类型)

1 public class GenericClass<E> {
2     private E e;
3     public GenericClass() {}
4     public GenericClass(E e) { this.e = e; }
5     public E getE() { return e; }
6     public void setE(E e) { this.e = e; }
7 }

接下来我使用这个泛型类,创建类时类型参数指定为String,类中的E相当于就替换成了String类型

1     public static void main(String[] args) {
2         GenericClass<String> obj = new GenericClass<>();
3         obj.setE("Hello world!");
4         System.out.println(obj.getE());
5     }

此外,还可以定义泛型接口和泛型方法↓

 泛型接口,接口的类型变量可以在实现类中指定,也可以将实现类定义成泛型类,创建对象时在指定

1 public interface Interface<E> {
2     public abstract void method(E e);
3 }

 泛型方法,类型变量放在返回值前面。

1     public <E> void method(E e){
2         System.out.println(e);
3     }
1         GenericMethod obj = new GenericMethod();
2         obj.method("Hello world!");
3         obj.method(123);

上面在调用method方法时,参数为“Hello world!”则方法的类型变量E为String。参数为123则方法的类型变量为Integer。

 类型变量的限定

1     public static <T> T max(T t1, T t2) {
2         if (t1.compareTo(t2) > 0) return t1;
3         else return t2;
4     }

看上面这段代码,t1调用了compareTo方法。因为变量t1的类型为T,这意味着它可以是任意类的对象,怎么确保T所属的类有compareTo方法呢?

我们必须对类型变量T进行限定: <T extends Comparable>  限定后,T只能是限定类型的子类型。

类型变量使用逗号分隔,限定类型使用&号分隔。

通配符类型, 允许类型参数发生变化。只能用于填充泛型变量T,表示通配任何类型。

1         ArrayList<? extends Number> list1;
2         list1 = new ArrayList<Integer>();

list1指向一个数组列表,这个列表中只能存Number类及其子类型。

通配符的限定与类型变量的限定类似,但可以使用super指定超类型限定。

1         ArrayList<? super Number> list2;

超类型限定后,list2指向的数组列表只能存Number类及其超类型。


 

集合

Collection接口

Collection接口是最基本的集合接口,一个 Collection 代表一组 Object,即 Collection 的元素, Java不提供直接继承自Collection的类,只提供继承于的子接口(如List和set)。Collection 接口存储一组不唯一无序的对象。

1         Collection coll = new ArrayList();
2         
3         coll.add("Hello world!");      //向集合中添加元素
4         coll.toString();           //返回包含所有元素的字符串
5         coll.remove("Hello world!");    //移除指定元素

List接口

List接口是一个有序的Collection,使用此接口能够精确的控制每个元素插入的位置,能够通过索引(元素在List中位置,类似于数组的下标)来访问List中的元素,第一个元素的索引为 0,而且允许有相同的元素。List 接口存储一组不唯一有序(插入顺序)的对象。

1. ArrayList 类继承了 AbstractList ,并实现了 List 接口,是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,我们可以添加或删除元素。

1         ArrayList list = new ArrayList();
2 
3         list.ensureCapacity(10);   //如有必要,增加此 ArrayList 实例的容量,以确保它至少能够容纳最小容量参数所指定的元素数。
4         list.trimToSize();      //将此 ArrayList 实例的容量调整为列表的当前大小。

2. 链表(Linked list)是一种双向线性链表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的地址。

1         LinkedList list = new LinkedList();
2 
3         list.addFirst("aaa");     //将指定元素插入此列表的开头。
4         list.push("ccc");         //将元素推入此列表所表示的堆栈。
5         list.pop();                  //从此列表所表示的堆栈处弹出一个元素。
6         list.listIterator(0);  //返回此列表中的元素的列表迭代器,从列表中指定位置开始。

Set接口

Set接口具有与Collection完全一样的接口,只是行为上不同,Set保存不重复的元素。Set接口存储一组唯一无序的对象。

1. HashSet 实现了 Set 接口,基于 HashMap 来实现的,是一个不允许有重复元素的集合。允许有 null 值,是无序的,即不会记录插入的顺序。不是线程安全的, 如果多个线程尝试同时修改 HashSet,则最终结果是不确定的,必须在多线程访问时显式同步对 HashSet 的并发访问。

1         HashSet list = new HashSet();
2 
3         list.iterator();//返回对此 set 中元素进行迭代的迭代器。

Map接口

Map接口存储一组键值对象,提供key(键)到value(值)的映射。

1. HashMap 是一个散列表,继承于AbstractMap,实现了 Map、Cloneable、java.io.Serializable 接口,它存储的内容是键值对(key-value)映射。根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。是无序的,即不会记录插入的顺序。(缺)


 多线程

创建线程目前我了解到3种方法

1. 通过继承Thread类创建线程

 1 public class myThread extends Thread {
 2     public myThread() {}
 3 
 4     public myThread(String name) { super(name); }
 5 
 6     //重写Thread类中的run方法
 7     @Override
 8     public void run(){
 9         System.out.println("Hello World!");
10     }
11 }

接下来使用新线程

1         Thread myThread = new myThread("myThread");
2         myThread.start();

上面这段代码我们新建了一个名为“myThread”的新线程,调用start方法启动该线程。start方法会调用run方法。

重写的run方法中的代码就是新线程要执行的代码。

2. 通过实现Runnable接口创建线程

1 public class ImplRunnable implements Runnable {
2     @Override
3     public void run(){
4         System.out.println("Hello World!");
5     }
6 }
1     Runnable r = new ImplRunnable();
2     Thread myThread = new Thread(r);
3     myThread.start();

3.通过实现Callable接口创建线程

1 public class ImplCallable implements Callable<String> {
2     @Override
3     public String call() throws Exception {
4         return "Hello World!";
5     }
6 }

实现Callable接口这种方法较为特别,它重写的是call方法。与前两种方法最大的不同是,这个线程执行完call中的代码后可以返回值,Callable的类型变量就是call方法返回值的类型,而且call方法需要声明异常。

1         Callable c = new ImplCallable();
2         FutureTask<String> ft = new FutureTask<>(c);
3         new Thread(ft).start();
4         System.out.println(ft.get());

同步

当多个线程同时操作一个可共享的资源变量时,将会导致数据不准确,相互之间产生冲突。

此时我们可以加入同步锁(同步监视器的锁定),以避免在该线程没有完成操作之前被其他线程调用。

线程同步的方法也了解到3种:

1     private int NUMBER = 100;//共享资源

1. 使用同步代码块: synchronized(obj){ }

 1 Object obj = new Object();//将这个Object对象作为同步锁
 2 
 3     @Override
 4     public void run(){
 5         while(true){
 6             synchronized (obj) {
 7                 if(NUMBER<=0) break;
 8                 System.out.println(Thread.currentThread().getName() + NUMBER);
 9                 NUMBER--;
10             }
11         }
12     }

2. 使用同步方法: public synchronized void method(){ }

 1     @Override
 2     public void run() {
 3         while (true) {
 4             if (NUMBER <= 0) break;
 5             method();
 6         }
 7     }
 8 
 9     public synchronized void method() {
10         if (NUMBER > 0) {
11             System.out.println(Thread.currentThread().getName() + NUMBER);
12             NUMBER--;
13         }
14     }

3. 使用Lock接口的锁: lock() 和 unlock()

 1     Lock theLock = new ReentrantLock();
 2 
 3     @Override
 4     public void run() {
 5         while (true) {
 6             theLock.lock();//获取lock锁
 7             if (NUMBER > 0) {
 8                 System.out.println(Thread.currentThread().getName() + NUMBER);
 9                 NUMBER--;
10             }
11             theLock.unlock();//释放lock锁
12             if (NUMBER <= 0) break;
13         }
14     }

线程间通信

使用wait/notify方法:

wait()方法:在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。

notify()方法:唤醒在此对象监视器上等待的单个线程。

线程池

创建一个线程池,需要线程拿一个出来用,使用完可以放回线程池(以类似队列的形式),不用一直创建销毁线程,提高代码效率。

1         //使用线程池的工厂类Executor中的静态方法newFixedThreadPool创建一个线程数为2的线程池.
2         ExecutorService es = Executors.newFixedThreadPool(2);
3         //调用线程池对象的submit()方法,传递线程任务,开启线程,执行run()方法.
4         //线程执行完会归还到线程池,可继续使用(类似队列机制)
5         es.submit(new ImplRunnable());
6         //线程池的关闭方法(不推荐使用?)
7         es.shutdown();

 


 

这篇随笔就先这样了,还有一些内容有机会再回来补了=_=

 

阅读(2164) 评论(0)