Java程序猿面试题集(51-70)
摘要:这一部分主要解说了异常、多线程、容器和I/O的相关面试题。首先,异常机制提供了一种在不打乱原有业务逻辑的前提下,把程序在执行时可能出现的状况处理掉的优雅的解决方式,同一时候也是面向对象的解决方式。而Java的线程模型是建立在共享的、默认的可见的可变状态以及抢占式线程调度两个概念之上的。
Java内置了对多线程编程的支持在20世纪90年代可以说是一个巨大的进步,可是最初的设计在当下看来已经给程序带来非常多困扰了。感谢Doug Lea在Java 5中提供了他里程碑式的杰作java.util.concurrent包,它的出现让Java的多线程编程可以更好的工作。
Java 1.4中引入NIO实现了对非堵塞I/O的支持。NIO为I/O操作抽象出缓冲区和通道层。攻克了字符集的编码和解码问题,提供了将文件映射为内存数据的接口。NIO无疑使Java向前迈出了一大步,但为了方便Java对文件系统的处理,NIO.2进一步对Java的I/O操作进行了增强。提供了能批量获取文件属性的文件系统接口。还提供了套接字和文件都能进行异步IO操作的API。完毕了JSR-51中定义的套接字。
对于Java中的容器(集合框架)而言。Java 5中引入泛型无疑是程序猿的福音,然而那不过糖衣语法,底层实现没有本质的区别,因此与C#相比。Java的泛型显得不那么让人痛快。
51、类ExampleA 继承Exception,类ExampleB 继承ExampleA。
有例如以下代码片断:
try{ throw new ExampleB("b")}catch(ExampleA e){ System.out.println("ExampleA");}catch(Exception e){ System.out.println("Exception");}请问运行此段代码的输出是什么?
答:输出:ExampleA。(依据里氏代换原则[能使用父类型的地方一定能使用子类型],抓取ExampleA类型异常的catch块可以抓住try块中抛出的ExampleB类型的异常)
补充:比此题略复杂的一道面试题例如以下所看到的(此题的出处是《Java编程思想》),说出你的答案吧!
class Annoyance extends Exception {}class Sneeze extends Annoyance {}class Human { public static void main(String[] args) throws Exception { try { try { throw new Sneeze(); } catch ( Annoyance a ) { System.out.println("Caught Annoyance"); throw a; } } catch ( Sneeze s ) { System.out.println("Caught Sneeze"); return ; } finally { System.out.println("Hello World!"); } }}52、List、Set、Map 是否继承自Collection 接口?
答:List、Set 是。Map 不是。
Map是键值对映射容器,与List和Set有明显的差别,而Set存储的零散的元素且不同意有反复元素(数学中的集合也是如此),List是线性结构的容器,适用于按数值索引訪问元素的情形。
53、说出ArrayList、Vector、LinkedList 的存储性能和特性?
答:ArrayList 和Vector都是使用数组方式存储数据。此数组元素数大于实际存储的数据以便添加和插入元素。它们都同意直接按序号索引元素。可是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector因为使用了synchronized 方法(线程安全),通常性能上较ArrayList 差。而LinkedList 使用双向链表实现存储(将内存中零散的内存单元通过附加的引用关联起来。形成一个能够按序号索引的线性结构。这样的链式存储方式与数组的连续存储方式相比,事实上对内存的利用率更高),按序号索引数据须要进行前向或后向遍历,可是插入数据时仅仅须要记录本项的前后项就可以,所以插入速度较快。Vector属于遗留容器(早期的JDK中使用的容器,除此之外Hashtable、Dictionary、BitSet、Stack、Properties都是遗留容器),如今已经不推荐使用,可是因为ArrayList和LinkedListed都是非线程安全的,假设须要多个线程操作同一个容器,那么能够通过工具类Collections中的synchronizedList方法将其转换成线程安全的容器后再使用(这事实上是装潢模式最好的样例,将已有对象传入还有一个类的构造器中创建新的对象来添加新功能)。
补充:遗留容器中的Properties类和Stack类在设计上有严重的问题。Properties是一个键和值都是字符串的特殊的键值对映射,在设计上应该是关联一个Hashtable并将其两个泛型參数设置为String类型,可是Java API中的Properties直接继承了Hashtable,这非常明显是对继承的滥用。这里复用代码的方式应该是HAS-A关系而不是IS-A关系,还有一方面容器都属于工具类,继承工具类本身就是一个错误的做法。使用工具类最好的方式是HAS-A关系(关联)或USE-A关系(依赖)。同理,Stack类继承Vector也是不对的。
54、Collection 和Collections 的差别?
答:Collection 是一个接口。它是Set、List等容器的父接口。Collections 是个一个工具类,提供了一系列的静态方法来辅助容器操作。这些方法包含对容器的搜索、排序、线程安全化等等。
55、List、Map、Set 三个接口。存取元素时。各有什么特点?
答:List以特定索引来存取元素。可有反复元素。Set不能存放反复元素(用对象的equals()方法来区分元素是否反复)。Map保存键值对(key-value pair)映射。映射关系能够是一对一或多对一。Set和Map容器都有基于哈希存储和排序树的两种实现版本号,基于哈希存储的版本号理论存取时间复杂度为O(1),而基于排序树版本号的实如今插入或删除元素时会依照元素或元素的键(key)构成排序树从而达到排序和去重的效果。
56、TreeMap和TreeSet在排序时怎样比較元素?Collections工具类中的sort()方法怎样比較元素?
答:TreeSet要求存放的对象所属的类必须实现Comparable接口,该接口提供了比較元素的compareTo()方法,当插入元素时会回调该方法比較元素的大小。TreeMap要求存放的键值对映射的键必须实现Comparable接口从而依据键对元素进行排序。Collections工具类的sort方法有两种重载的形式,第一种要求传入的待排序容器中存放的对象比較实现Comparable接口以实现元素的比較;另外一种不强制性的要求容器中的元素必须可比較。可是要求传入第二个參数。參数是Comparator接口的子类型(须要重写compare方法实现元素的比較),相当于一个暂时定义的排序规则。事实上就是是通过接口注入比較元素大小的算法,也是对回调模式的应用。
样例1:
Student.java
package com.lovo.demo;public class Student implements ComparableTest01.java{ private String name; // 姓名 private int age; // 年龄 public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student [name=" + name + ", age=" + age + "]"; } @Override public int compareTo(Student o) { return this.age - o.age; // 比較年龄(年龄的升序) }}
package com.lovo.demo;import java.util.Set;import java.util.TreeSet;class Test01 { public static void main(String[] args) { Set样例2:set = new TreeSet<>(); // Java 7的钻石语法(构造器后面的尖括号里不须要写类型) set.add(new Student("Hao LUO", 33)); set.add(new Student("XJ WANG", 32)); set.add(new Student("Bruce LEE", 60)); set.add(new Student("Bob YANG", 22)); for(Student stu : set) { System.out.println(stu); }// 输出结果: // Student [name=Bob YANG, age=22]// Student [name=XJ WANG, age=32]// Student [name=Hao LUO, age=33]// Student [name=Bruce LEE, age=60] }}
Student.java
package com.lovo.demo;public class Student { private String name; // 姓名 private int age; // 年龄 public Student(String name, int age) { this.name = name; this.age = age; } /** * 获取学生姓名 */ public String getName() { return name; } /** * 获取学生年龄 */ public int getAge() { return age; } @Override public String toString() { return "Student [name=" + name + ", age=" + age + "]"; }}Test02.java
package com.lovo.demo;import java.util.ArrayList;import java.util.Collections;import java.util.Comparator;import java.util.List;class Test02 { public static void main(String[] args) { List57、sleep()和wait()有什么差别?list = new ArrayList<>(); // Java 7的钻石语法(构造器后面的尖括号里不须要写类型) list.add(new Student("Hao LUO", 33)); list.add(new Student("XJ WANG", 32)); list.add(new Student("Bruce LEE", 60)); list.add(new Student("Bob YANG", 22)); // 通过sort方法的第二个參数传入一个Comparator接口对象 // 相当于是传入一个比較对象大小的算法到sort方法中 // 因为Java中没有函数指针、仿函数、托付这种概念 // 因此要将一个算法传入一个方法中唯一的选择就是通过接口回调 Collections.sort(list, new Comparator () { @Override public int compare(Student o1, Student o2) { return o1.getName().compareTo(o2.getName()); // 比較学生姓名 } }); for(Student stu : list) { System.out.println(stu); }// 输出结果: // Student [name=Bob YANG, age=22]// Student [name=Bruce LEE, age=60]// Student [name=Hao LUO, age=33]// Student [name=XJ WANG, age=32] }}
答:sleep()方法是线程类(Thread)的静态方法,导致此线程暂停运行指定时间,将运行机会给其它线程。可是监控状态依旧保持,到时后会自己主动恢复(线程回到就绪(ready)状态)。由于调用sleep 不会释放对象锁。wait()是Object 类的方法。对此对象调用wait()方法导致本线程放弃对象锁(线程暂停运行),进入等待此对象的等待锁定池,仅仅有针对此对象发出notify 方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入就绪状态。
补充:这里似乎漏掉了一个作为先决条件的问题,就是什么是进程,什么是线程?为什么须要多线程编程?答案例如以下所看到的:
进程是具有一定独立功能的程序关于某个数据集合上的一次执行活动,是操作系统进行资源分配和调度的一个独立单位。线程是进程的一个实体,是CPU调度和分派的基本单位,是比进程更小的能独立执行的基本单位。线程的划分尺度小于进程。这使得多线程程序的并发性高;进程在执行时通常拥有独立的内存单元,而线程之间可以共享内存。
使用多线程的编程通常可以带来更好的性能和用户体验,可是多线程的程序对于其它程序是不友好的,由于它占用了很多其它的CPU资源。
58、sleep()和yield()有什么差别?
答:
① sleep()方法给其它线程执行机会时不考虑线程的优先级,因此会给低优先级的线程以执行的机会。yield()方法仅仅会给同样优先级或更高优先级的线程以执行的机会;
② 线程运行sleep()方法后转入堵塞(blocked)状态。而运行yield()方法后转入就绪(ready)状态。
③ sleep()方法声明抛出InterruptedException。而yield()方法没有声明不论什么异常。
④ sleep()方法比yield()方法(跟操作系统相关)具有更好的可移植性。
59、当一个线程进入一个对象的synchronized方法A之后。其他线程是否可进入此对象的synchronized方法?
答:不能。其他线程仅仅能訪问该对象的非同步方法,同步方法则不能进入。
60、请说出与线程同步相关的方法。
答:
- wait():使一个线程处于等待(堵塞)状态,并且释放所持有的对象的锁;
- sleep():使一个正在执行的线程处于睡眠状态,是一个静态方法。调用此方法要捕捉InterruptedException 异常。
- notify():唤醒一个处于等待状态的线程。当然在调用此方法的时候。并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,并且与优先级无关;
- notityAll():唤醒全部处入等待状态的线程,注意并非给全部唤醒线程一个对象的锁。而是让它们竞争;
- JDK 1.5通过Lock接口提供了显式(explicit)的锁机制,增强了灵活性以及对线程的协调。
Lock接口中定义了加锁(lock())和解锁(unlock())的方法。同一时候还提供了newCondition()方法来产生用于线程之间通信的Condition对象;
- JDK 1.5还提供了信号量(semaphore)机制,信号量能够用来限制对某个共享资源进行訪问的线程的数量。在对资源进行訪问之前。线程必须得到信号量的许可(调用Semaphore对象的acquire()方法)。在完毕对资源的訪问后,线程必须向信号量归还许可(调用Semaphore对象的release()方法)。
package com.lovo;/** * 银行账户 * @author 骆昊 * */public class Account { private double balance; // 账户剩余金额 /** * 存款 * @param money 存入金额 */ public void deposit(double money) { double newBalance = balance + money; try { Thread.sleep(10); // 模拟此业务须要一段处理时间 } catch(InterruptedException ex) { ex.printStackTrace(); } balance = newBalance; } /** * 获得账户剩余金额 */ public double getBalance() { return balance; }}存钱线程类:
package com.lovo;/** * 存钱线程 * @author 骆昊 * */public class AddMoneyThread implements Runnable { private Account account; // 存入账户 private double money; // 存入金额 public AddMoneyThread(Account account, double money) { this.account = account; this.money = money; } @Override public void run() { account.deposit(money); }}測试类:
package com.lovo;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class Test01 { public static void main(String[] args) { Account account = new Account(); ExecutorService service = Executors.newFixedThreadPool(100); for(int i = 1; i <= 100; i++) { service.execute(new AddMoneyThread(account, 1)); } service.shutdown(); while(!service.isTerminated()) {} System.out.println("账户剩余金额: " + account.getBalance()); }}在没有同步的情况下,运行结果一般是显示账户剩余金额在10元下面,出现这样的状况的原因是,当一个线程A试图存入1元的时候,另外一个线程B也可以进入存款的方法中,线程B读取到的账户剩余金额仍然是线程A存入1元钱之前的账户剩余金额,因此也是在原来的剩余金额0上面做了加1元的操作,同理线程C也会做类似的事情,所以最后100个线程运行结束时,本来期望账户剩余金额为100元。但实际得到的通常在10元下面。解决问题的办法就是同步。当一个线程对银行账户存钱时。须要将此账户锁定,待其操作完毕后才同意其它的线程进行操作,代码有例如以下几种调整方案:
package com.lovo;/** * 银行账户 * @author 骆昊 * */public class Account { private double balance; // 账户剩余金额 /** * 存款 * @param money 存入金额 */ public synchronized void deposit(double money) { double newBalance = balance + money; try { Thread.sleep(10); // 模拟此业务须要一段处理时间 } catch(InterruptedException ex) { ex.printStackTrace(); } balance = newBalance; } /** * 获得账户剩余金额 */ public double getBalance() { return balance; }}2. 在线程调用存款方法时对银行账户进行同步
package com.lovo;/** * 存钱线程 * @author 骆昊 * */public class AddMoneyThread implements Runnable { private Account account; // 存入账户 private double money; // 存入金额 public AddMoneyThread(Account account, double money) { this.account = account; this.money = money; } @Override public void run() { synchronized (account) { account.deposit(money); } }}3. 通过JDK 1.5显示的锁机制,为每一个银行账户创建一个锁对象。在存款操作进行加锁和解锁的操作
package com.lovo;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/** * 银行账户 * * @author 骆昊 * */public class Account { private Lock accountLock = new ReentrantLock(); private double balance; // 账户剩余金额 /** * 存款 * * @param money * 存入金额 */ public void deposit(double money) { accountLock.lock(); try { double newBalance = balance + money; try { Thread.sleep(10); // 模拟此业务须要一段处理时间 } catch (InterruptedException ex) { ex.printStackTrace(); } balance = newBalance; } finally { accountLock.unlock(); } } /** * 获得账户剩余金额 */ public double getBalance() { return balance; }}依照上述三种方式对代码进行改动后,重写运行測试代码Test01。将看到终于的账户剩余金额为100元。
61、编写多线程程序有几种实现方式?
答:Java 5曾经实现多线程有两种实现方法:一种是继承Thread类。还有一种是实现Runnable接口。
两种方式都要通过重写run()方法来定义线程的行为。推荐使用后者,由于Java中的继承是单继承,一个类有一个父类,假设继承了Thread类就无法再继承其它类了,显然使用Runnable接口更为灵活。
补充:Java 5以后创建线程还有第三种方式:实现Callable接口,该接口中的call方法能够在线程运行结束时产生一个返回值,代码例如以下所看到的:
package com.lovo.demo;import java.util.ArrayList;import java.util.List;import java.util.concurrent.Callable;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Future;class MyTask implements Callable{ private int upperBounds; public MyTask(int upperBounds) { this.upperBounds = upperBounds; } @Override public Integer call() throws Exception { int sum = 0; for(int i = 1; i <= upperBounds; i++) { sum += i; } return sum; } }public class Test { public static void main(String[] args) throws Exception { List > list = new ArrayList<>(); ExecutorService service = Executors.newFixedThreadPool(10); for(int i = 0; i < 10; i++) { list.add(service.submit(new MyTask((int) (Math.random() * 100)))); } int sum = 0; for(Future future : list) { while(!future.isDone()) ; sum += future.get(); } System.out.println(sum); }}
62、synchronizedkeyword的使用方法?
答:synchronizedkeyword能够将对象或者方法标记为同步,以实现对对象和方法的相互排斥訪问。能够用synchronized(对象) { … }定义同步代码块,或者在声明方法时将synchronized作为方法的修饰符。在第60题的样例中已经展示了synchronizedkeyword的使用方法。
63、举例说明同步和异步。
答:假设系统中存在临界资源(资源数量少于竞争资源的线程数量的资源),比如正在写的数据以后可能被还有一个线程读到,或者正在读的数据可能已经被还有一个线程写过了,那么这些数据就必须进行同步存取(数据库操作中的悲观锁就是最好的样例)。当应用程序在对象上调用了一个须要花费非常长时间来运行的方法。而且不希望让程序等待方法的返回时,就应该使用异步编程,在非常多情况下採用异步途径往往更有效率。
其实,所谓的同步就是指堵塞式操作,而异步就是非堵塞式操作。
64、启动一个线程是用run()还是start()方法?
答:启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可执行状态。这意味着它能够由JVM 调度并执行,这并不意味着线程就会马上执行。run()方法是线程启动后要进行回调(callback)的方法。
65、什么是线程池(thread pool)?
答:在面向对象编程中,创建和销毁对象是非常费时间的,由于创建一个对象要获取内存资源或者其他很多其他资源。
在Java中更是如此。虚拟机将试图跟踪每个对象,以便可以在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能降低创建和销毁对象的次数,特别是一些非常耗资源的对象创建和销毁,这就是"池化资源"技术产生的原因。线程池顾名思义就是事先创建若干个可运行的线程放入一个池(容器)中。须要的时候从池中获取线程不用自行创建,使用完成不须要销毁线程而是放回池中,从而降低创建和销毁线程对象的开销。
Java 5+中的Executor接口定义一个运行线程的工具。
它的子类型即线程池接口是ExecutorService。
要配置一个线程池是比較复杂的,尤其是对于线程池的原理不是非常清楚的情况下,因此在工具类Executors面提供了一些静态工厂方法,生成一些经常使用的线程池,例如以下所看到的:
- newSingleThreadExecutor:创建一个单线程的线程池。这个线程池仅仅有一个线程在工作,也就是相当于单线程串行运行全部任务。假设这个唯一的线程由于异常结束。那么会有一个新的线程来替代它。
此线程池保证全部任务的运行顺序依照任务的提交顺序运行。
- newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
线程池的大小一旦达到最大值就会保持不变。假设某个线程由于运行异常而结束,那么线程池会补充一个新线程。
- newCachedThreadPool:创建一个可缓存的线程池。假设线程池的大小超过了处理任务所须要的线程。那么就会回收部分空暇(60秒不运行任务)的线程,当任务数添加时,此线程池又可以智能的加入新线程来处理任务。此线程池不会对线程池大小做限制。线程池大小全然依赖于操作系统(或者说JVM)可以创建的最大线程大小。
- newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性运行任务的需求。
- newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性运行任务的需求。
66、线程的基本状态以及状态之间的关系?
答:
除去起始(new)状态和结束(finished)状态,线程有三种状态。各自是:就绪(ready)、执行(running)和堵塞(blocked)。当中就绪状态代表线程具备了执行的全部条件。仅仅等待CPU调度(万事俱备。仅仅欠东风);处于执行状态的线程可能由于CPU调度(时间片用完了)的原因回到就绪状态。也有可能由于调用了线程的yield方法回到就绪状态,此时线程不会释放它占有的资源的锁,坐等CPU以继续执行;执行状态的线程可能由于I/O中断、线程休眠、调用了对象的wait方法而进入堵塞状态(有的地方也称之为等待状态);而进入堵塞状态的线程会由于休眠结束、调用了对象的notify方法或notifyAll方法或其它线程执行结束而进入就绪状态。注意:调用wait方法会让线程进入等待池中等待被唤醒,notify方法或notifyAll方法会让等待锁中的线程从等待池进入等锁池,在没有得到对象的锁之前,线程仍然无法获得CPU的调度和执行。
67、简述synchronized 和java.util.concurrent.locks.Lock的异同?
答:Lock是Java 5以后引入的新的API,和keywordsynchronized相比主要同样点:Lock 能完毕synchronized所实现的全部功能;主要不同点:Lock 有比synchronized 更精确的线程语义和更好的性能。
synchronized 会自己主动释放锁,而Lock 一定要求程序猿手工释放,而且必须在finally 块中释放(这是释放外部资源的最好的地方)。
68、Java中怎样实现序列化,有什么意义?
答:序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。
能够对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决对象流读写操作时可能引发的问题(假设不进行序列化可能会存在数据乱序的问题)。
要实现序列化。须要让一个类实现Serializable接口,该接口是一个标识性接口。标注该类对象是可被序列化的,然后使用一个输出流来构造一个对象输出流并通过writeObject(Object obj)方法就行将实现对象写出(即保存其状态)。假设须要反序列化则可以用一个输入流建立对象输入流。然后通过readObject方法从流中读取对象。
序列化除了可以实现对象的持久化之外。还可以用于对象的深度克隆(參见Java面试题集1-29题)
69、Java 中有几种类型的流?
答:字节流,字符流。
字节流继承于InputStream、OutputStream。字符流继承于Reader、Writer。
在java.io 包中还有更多的流,主要是为了提高性能和使用方便。
补充:关于Java的IO须要注意的有两点:一是两种对称性(输入和输出的对称性,字节和字符的对称性);二是两种设计模式(适配器模式和装潢模式)。另外Java中的流不同于C#的是它仅仅有一个维度一个方向。
补充:以下用IO和NIO两种方式实现文件拷贝,这个题目在面试的时候是常常被问到的。
package com.lovo;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;public class MyUtil { private MyUtil() { throw new AssertionError(); } public static void fileCopy(String source, String target) throws IOException { try (InputStream in = new FileInputStream(source)) { try (OutputStream out = new FileOutputStream(target)) { byte[] buffer = new byte[4096]; int bytesToRead; while((bytesToRead = in.read(buffer)) != -1) { out.write(buffer, 0, bytesToRead); } } } } public static void fileCopyNIO(String source, String target) throws IOException { try (FileInputStream in = new FileInputStream(source)) { try (FileOutputStream out = new FileOutputStream(target)) { FileChannel inChannel = in.getChannel(); FileChannel outChannel = out.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(4096); while(inChannel.read(buffer) != -1) { buffer.flip(); outChannel.write(buffer); buffer.clear(); } } } }}
注意:上面用到Java 7的TWR。使用TWR后能够不用在finally中释放外部资源 ,从而让代码更加优雅。
70、写一个方法,输入一个文件名称和一个字符串,统计这个字符串在这个文件里出现的次数。
答:代码例如以下:
package com.lovo.demo;import java.io.BufferedReader;import java.io.FileReader;public class MyUtil { // 工具类中的方法都是静态方式訪问的因此将构造器私有不同意创建对象(绝对好习惯) private MyUtil() { throw new AssertionError(); } /** * 统计给定文件里给定字符串的出现次数 * * @param filename 文件名称 * @param word 字符串 * @return 字符串在文件里出现的次数 */ public static int countWordInFile(String filename, String word) { int counter = 0; try (FileReader fr = new FileReader(filename)) { try (BufferedReader br = new BufferedReader(fr)) { String line = null; while ((line = br.readLine()) != null) { int index = -1; while (line.length() >= word.length() && (index = line.indexOf(word)) >= 0) { counter++; line = line.substring(index + word.length()); } } } } catch (Exception ex) { ex.printStackTrace(); } return counter; }}