对象与类
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();
这篇随笔就先这样了,还有一些内容有机会再回来补了=_=