Threading overhaul + Updated datasaving
Data-saving at high cycle count takes too long, causing the DUT to fall asleep. Data-saving has been extracted into its own thread. Additionally, Run/pause switch has been re-threaded using synchronized statements, rather than lock statements, making the run switch work properly (finally). Data is now saved in the new Cycle object, rather than passing around multiple maps, hopefully improving memory footprint.
This commit is contained in:
parent
52fe20fba5
commit
e37f7b71f9
9 changed files with 246 additions and 108 deletions
|
@ -4,7 +4,7 @@
|
|||
<groupId>org.baxter.disco</groupId>
|
||||
<artifactId>ocr</artifactId>
|
||||
<name>Disco OCR Accuracy Over Life Testing</name>
|
||||
<version>4.3.8</version>
|
||||
<version>4.3.9</version>
|
||||
<description>Testing Discos for long-term accuracy, using automated optical character recognition.</description>
|
||||
<organization>
|
||||
<name>Baxter International</name>
|
||||
|
@ -95,9 +95,6 @@
|
|||
<plugin>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.4.1</version>
|
||||
<configuration>
|
||||
<show>private</show>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</reporting>
|
||||
|
|
2
pom.xml
2
pom.xml
|
@ -3,7 +3,7 @@
|
|||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.baxter.disco</groupId>
|
||||
<artifactId>ocr</artifactId>
|
||||
<version>4.3.8</version>
|
||||
<version>4.3.9</version>
|
||||
<packaging>jar</packaging>
|
||||
<name>Disco OCR Accuracy Over Life Testing</name>
|
||||
<description>Testing Discos for long-term accuracy, using automated optical character recognition.</description>
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
#! /usr/bin/env sh
|
||||
sudo java -jar discoTesting-4.3.8.jar 2>/dev/null
|
||||
sudo java -jar discoTesting-4.3.9.jar 2>/dev/null
|
||||
|
|
|
@ -3,12 +3,11 @@ package org.baxter.disco.ocr;
|
|||
//Standard imports
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Scanner;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* CLI for the Fixture.
|
||||
|
@ -18,20 +17,22 @@ import java.util.concurrent.locks.ReentrantLock;
|
|||
* classes).
|
||||
*
|
||||
* @author Blizzard Finnegan
|
||||
* @version 1.8.0, 16 Mar. 2023
|
||||
* @version 1.9.0, 20 Mar. 2023
|
||||
*/
|
||||
public class Cli
|
||||
{
|
||||
/**
|
||||
* Complete build version number
|
||||
*/
|
||||
private static final String version = "4.3.8";
|
||||
private static final String version = "4.3.9";
|
||||
|
||||
/**
|
||||
* Currently saved iteration count.
|
||||
*/
|
||||
private static int iterationCount = 10;
|
||||
|
||||
private static boolean endOfCycles = false;
|
||||
|
||||
/**
|
||||
* Scanner used for monitoring user input.
|
||||
* This is a global object, so that functions
|
||||
|
@ -65,11 +66,6 @@ public class Cli
|
|||
*/
|
||||
private static final int cameraMenuOptionCount = 7;
|
||||
|
||||
/**
|
||||
* Lock object, used for temporary interruption of {@link #runTests()}
|
||||
*/
|
||||
public static final Lock LOCK = new ReentrantLock();
|
||||
|
||||
|
||||
static
|
||||
{
|
||||
|
@ -139,6 +135,7 @@ public class Cli
|
|||
setActiveCameras();
|
||||
break;
|
||||
case 5:
|
||||
endOfCycles = false;
|
||||
if(!camerasConfigured)
|
||||
{
|
||||
prompt("You have not configured the cameras yet! Are you sure you would like to continue? (y/N): ");
|
||||
|
@ -647,21 +644,34 @@ public class Cli
|
|||
try{ Thread.sleep(2000); } catch(Exception e){ ErrorLogging.logError(e); }
|
||||
}
|
||||
|
||||
Map<File,Double> resultMap = new HashMap<>();
|
||||
Map<String,File> cameraToFile = new HashMap<>();
|
||||
|
||||
//Initialise cameraToFile, so keys don't shuffle.
|
||||
for(String cameraName : cameraList)
|
||||
{
|
||||
cameraToFile.put(cameraName,new File("/dev/null"));
|
||||
}
|
||||
final Map<String,String> serials = ConfigFacade.getSerials();
|
||||
|
||||
ErrorLogging.logError("DEBUG: Starting tests...");
|
||||
|
||||
//All portions of the test check with the GPIO Run/Pause switch before
|
||||
//continuing, using the Lock object.
|
||||
|
||||
final LinkedBlockingQueue<Cycle> dataEntryQueue = new LinkedBlockingQueue<>();
|
||||
|
||||
Thread writeThread = new Thread( () -> {
|
||||
while(dataEntryQueue.size() > 0 && !endOfCycles)
|
||||
{
|
||||
Cycle cycle = null;
|
||||
|
||||
do
|
||||
{
|
||||
try{ cycle = dataEntryQueue.poll(Long.MAX_VALUE,TimeUnit.SECONDS); }
|
||||
catch(Exception e){ ErrorLogging.logError(e); }
|
||||
}
|
||||
while(cycle == null);
|
||||
|
||||
DataSaving.writeValues(cycle,serials);
|
||||
}
|
||||
});
|
||||
|
||||
writeThread.start();
|
||||
|
||||
for(int i = 0; i < localIterations; i++)
|
||||
{
|
||||
Cycle cycle = new Cycle(i);
|
||||
println("");
|
||||
ErrorLogging.logError("====================================");
|
||||
ErrorLogging.logError("Starting iteration " + (i+1) + " of " + localIterations + "...");
|
||||
|
@ -675,9 +685,7 @@ public class Cli
|
|||
fail = false;
|
||||
if(safeGPIO)
|
||||
{
|
||||
while(!LOCK.tryLock()) {}
|
||||
MovementFacade.iterationMovement(prime);
|
||||
LOCK.unlock();
|
||||
|
||||
//Wait for the DUT to display an image
|
||||
try{ Thread.sleep(2000); } catch(Exception e){ ErrorLogging.logError(e); }
|
||||
|
@ -685,27 +693,19 @@ public class Cli
|
|||
|
||||
for(String cameraName : cameraList)
|
||||
{
|
||||
while(!LOCK.tryLock()) {}
|
||||
File file = OpenCVFacade.completeProcess(cameraName);
|
||||
LOCK.unlock();
|
||||
|
||||
while(!LOCK.tryLock()) {}
|
||||
cameraToFile.replace(cameraName,file);
|
||||
LOCK.unlock();
|
||||
Thread cameraThread = new Thread(() -> {
|
||||
File file = OpenCVFacade.completeProcess(cameraName);
|
||||
Double result = TesseractFacade.imageToDouble(file);
|
||||
ErrorLogging.logError("Parsed value from camera " + cameraName +": " + result);
|
||||
synchronized(cycle) { cycle.addCamera(cameraName,file,result); }
|
||||
});
|
||||
cameraThread.start();
|
||||
try{ cameraThread.join(); } catch(Exception e){ ErrorLogging.logError(e); }
|
||||
}
|
||||
|
||||
for(String cameraName : cameraList)
|
||||
{
|
||||
while(!LOCK.tryLock()) {}
|
||||
File file = cameraToFile.get(cameraName);
|
||||
LOCK.unlock();
|
||||
while(!LOCK.tryLock()) {}
|
||||
Double result = TesseractFacade.imageToDouble(file);
|
||||
LOCK.unlock();
|
||||
while(!LOCK.tryLock()) {}
|
||||
resultMap.put(file,result);
|
||||
ErrorLogging.logError("Parsed value from camera " + cameraName +": " + result);
|
||||
LOCK.unlock();
|
||||
double result = cycle.getValue(cameraName);
|
||||
if(result <= 10 ||
|
||||
result >= 100 ||
|
||||
result == Double.NEGATIVE_INFINITY)
|
||||
|
@ -723,14 +723,9 @@ public class Cli
|
|||
}
|
||||
}
|
||||
while(fail);
|
||||
|
||||
while(!LOCK.tryLock()) {}
|
||||
DataSaving.writeValues(i,resultMap,cameraToFile);
|
||||
LOCK.unlock();
|
||||
|
||||
//DO NOT CLEAR camera to file Map. This will change the order of the objects within it
|
||||
resultMap.clear();
|
||||
dataEntryQueue.add(cycle);
|
||||
}
|
||||
endOfCycles = true;
|
||||
println("=======================================");
|
||||
println("Testing complete!");
|
||||
}
|
||||
|
|
84
src/main/java/org/baxter/disco/ocr/Cycle.java
Normal file
84
src/main/java/org/baxter/disco/ocr/Cycle.java
Normal file
|
@ -0,0 +1,84 @@
|
|||
package org.baxter.disco.ocr;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* A single cycle in a test.
|
||||
*
|
||||
* @author Blizzard Finnegan
|
||||
* @version 1.0.0, 20 Mar. 2023
|
||||
*/
|
||||
public class Cycle
|
||||
{
|
||||
/**
|
||||
* Current cycle number.
|
||||
*/
|
||||
private final int cycleNumber;
|
||||
|
||||
/**
|
||||
* Map of values, paired to their camera.
|
||||
*
|
||||
* Key: String of the name of the camera
|
||||
* Value: Pair[File,Double] of the information for this cycle.
|
||||
*
|
||||
* Pair[File,Double]:
|
||||
* First: File of the location of the image from the camera.
|
||||
* Second: Double generated by Tesseract parsing the above image.
|
||||
*/
|
||||
private final Map<String,Pair<File,Double>> cycleValues = new TreeMap<>();
|
||||
|
||||
/**
|
||||
* Default constructor.
|
||||
*/
|
||||
public Cycle(int cycleNumber)
|
||||
{ this.cycleNumber = cycleNumber; }
|
||||
|
||||
/**
|
||||
* Getter for this object's cycle number.
|
||||
*
|
||||
* @return int of the current cycle number.
|
||||
*/
|
||||
public int getcycleNumber() { return this.cycleNumber; }
|
||||
|
||||
/**
|
||||
* Get all cameras contained within this object.
|
||||
*
|
||||
* @return Set of Strings of all cameras available in this object.
|
||||
*/
|
||||
public Set<String> getCameras() { return this.cycleValues.keySet(); }
|
||||
|
||||
/**
|
||||
* Getter for the given camera's associated image File.
|
||||
*
|
||||
* @param cameraName name of the camera to get the image from
|
||||
*
|
||||
* @return File of the image from the camera
|
||||
*/
|
||||
public File getImage(String cameraName)
|
||||
{ return this.cycleValues.get(cameraName).first(); }
|
||||
|
||||
/**
|
||||
* Get the Tesseract parsed value of the image from the given camera.
|
||||
*
|
||||
* @param cameraName name of the camera to get the image from
|
||||
*
|
||||
* @return Double of the Tesseract-parsed value of the image.
|
||||
*/
|
||||
public Double getValue(String cameraName)
|
||||
{ return this.cycleValues.get(cameraName).second(); }
|
||||
|
||||
/**
|
||||
* Add a camera to the object, with its accociated image and value.
|
||||
*
|
||||
* @param cameraName Name of the camera associated with the image and value
|
||||
* @param image File object denoting the location of the image from the camera
|
||||
* @param parsedValue Double of the value generated by Tesseract from the image
|
||||
*
|
||||
* @return true if saved successfully, else false.
|
||||
*/
|
||||
public void addCamera(String cameraName, File image, Double parsedValue)
|
||||
{ this.cycleValues.put(cameraName,new Pair<File,Double>(image, parsedValue)); }
|
||||
}
|
|
@ -32,7 +32,7 @@ import static org.apache.poi.hssf.util.HSSFColor.HSSFColorPredefined;
|
|||
* Facade for saving data out to a file.
|
||||
*
|
||||
* @author Blizzard Finnegan
|
||||
* @version 5.0.1, 10 Mar. 2023
|
||||
* @version 6.0.0, 10 Mar. 2023
|
||||
*/
|
||||
public class DataSaving
|
||||
{
|
||||
|
@ -167,7 +167,7 @@ public class DataSaving
|
|||
*
|
||||
* @param cameraCount The number of cameras that were used.
|
||||
*/
|
||||
private static void updateFormulas(int cameraCount)
|
||||
private static synchronized void updateFormulas(int cameraCount)
|
||||
{
|
||||
ErrorLogging.logError("DEBUG: Updating formulas in Excel sheet...");
|
||||
int rowIndex = 0;
|
||||
|
@ -227,30 +227,29 @@ public class DataSaving
|
|||
* Writes line to XLSX file.
|
||||
*
|
||||
* @param cycle What test cycle is being saved to the file
|
||||
* @param inputMap Map[String,Double] list of inputs
|
||||
* @param serials Map[String,String] list of camera serials
|
||||
*
|
||||
* @return Returns whether values were saved successfully.
|
||||
*/
|
||||
public static boolean writeValues(int cycle, Map<File,Double> inputMap, Map<String,File> cameraToFile)
|
||||
public static synchronized boolean writeValues(Cycle cycle, Map<String,String> serials)
|
||||
{
|
||||
ErrorLogging.logError("DEBUG: Writing values for " + (cycle + 1) + " cycle to worksheet.");
|
||||
int cycleNumber = cycle.getcycleNumber() + 1;
|
||||
ErrorLogging.logError("DEBUG: Writing values for " + (cycleNumber) + " cycle to worksheet.");
|
||||
boolean output = false;
|
||||
int cellnum = 0;
|
||||
int startingRow = outputSheet.getLastRowNum();
|
||||
HSSFRow row = (cycle == 1) ? outputSheet.getRow(startingRow) : outputSheet.createRow(++startingRow);
|
||||
List<String> cameraNames = new ArrayList<>(cameraToFile.keySet());
|
||||
|
||||
cycle++;
|
||||
HSSFRow row = (cycle.getcycleNumber() == 1) ? outputSheet.getRow(startingRow) : outputSheet.createRow(++startingRow);
|
||||
List<String> cameraNames = new ArrayList<>(cycle.getCameras());
|
||||
|
||||
HSSFCell indexCell = row.createCell(cellnum++);
|
||||
indexCell.setCellValue(cycle);
|
||||
indexCell.setCellValue(cycleNumber);
|
||||
for(String cameraName : cameraNames)
|
||||
{
|
||||
String serialNumber = ConfigFacade.getSerial(cameraName);
|
||||
String serialNumber = serials.get(cameraName);
|
||||
HSSFCell serialCell = row.createCell(cellnum++);
|
||||
serialCell.setCellValue(serialNumber);
|
||||
|
||||
File file = cameraToFile.get(cameraName);
|
||||
File file = cycle.getImage(cameraName);
|
||||
HSSFCell imageCell = row.createCell(cellnum++);
|
||||
try
|
||||
{
|
||||
|
@ -275,7 +274,7 @@ public class DataSaving
|
|||
|
||||
//Put the OCR value into the sheet
|
||||
HSSFCell ocrCell = row.createCell(cellnum++);
|
||||
Double ocrRead = inputMap.get(file);
|
||||
Double ocrRead = cycle.getValue(cameraName);
|
||||
if(ocrRead.equals(Double.NEGATIVE_INFINITY))
|
||||
{
|
||||
ocrCell.setCellValue("ERROR!");
|
||||
|
|
|
@ -87,7 +87,7 @@ public class ErrorLogging
|
|||
*
|
||||
* @param error Any error being thrown to be logged.
|
||||
*/
|
||||
public static void logError(Throwable error)
|
||||
public static synchronized void logError(Throwable error)
|
||||
{
|
||||
String errorStackTrace = ExceptionUtils.getStackTrace(error);
|
||||
String errorMessage = datetime.format(LocalDateTime.now()) + " - " + errorStackTrace;
|
||||
|
@ -100,7 +100,7 @@ public class ErrorLogging
|
|||
*
|
||||
* @param error Any information to save to the log file.
|
||||
*/
|
||||
public static void logError(String error)
|
||||
public static synchronized void logError(String error)
|
||||
{
|
||||
String errorMessage = datetime.format(LocalDateTime.now()) + "\t- " + error;
|
||||
fileOut.println(errorMessage);
|
||||
|
|
|
@ -17,7 +17,7 @@ import com.pi4j.io.gpio.digital.PullResistance;
|
|||
* Currently missing Run switch compatibility.
|
||||
*
|
||||
* @author Blizzard Finnegan
|
||||
* @version 3.1.0, 16 Mar. 2023
|
||||
* @version 3.3.0, 20 Mar. 2023
|
||||
*/
|
||||
public class MovementFacade
|
||||
{
|
||||
|
@ -32,6 +32,11 @@ public class MovementFacade
|
|||
*/
|
||||
private static Thread runSwitchThread;
|
||||
|
||||
/**
|
||||
* Lock object for multithreading/run switch.
|
||||
*/
|
||||
private static final Object lockObject = new Object();
|
||||
|
||||
/**
|
||||
* Fraction of the total travel time, so the arm won't push through the limit switch.
|
||||
*/
|
||||
|
@ -185,23 +190,27 @@ public class MovementFacade
|
|||
ErrorLogging.logError("DEBUG: Starting lock thread...");
|
||||
runSwitchThread = new Thread(() ->
|
||||
{
|
||||
boolean unlock = false;
|
||||
while(!exit)
|
||||
{
|
||||
if(runSwitch.isOn())
|
||||
{
|
||||
ErrorLogging.logError("DEBUG: Run switch turned off!");
|
||||
while(!Cli.LOCK.tryLock())
|
||||
{}
|
||||
unlock = true;
|
||||
synchronized(lockObject)
|
||||
{
|
||||
while(runSwitch.isOn())
|
||||
{
|
||||
try{ Thread.sleep(100); } catch(Exception e){ErrorLogging.logError(e);}
|
||||
if(runSwitch.isOff())
|
||||
{
|
||||
try{ Thread.sleep(100); } catch(Exception e){ErrorLogging.logError(e);}
|
||||
if(runSwitch.isOff())
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//ErrorLogging.logError("Run switch on!");
|
||||
if(unlock)
|
||||
{ Cli.LOCK.unlock(); unlock = false; }
|
||||
}
|
||||
try{ Thread.sleep(100); } catch(Exception e) { ErrorLogging.logError(e); }
|
||||
try{ Thread.sleep(1); } catch(Exception e){ErrorLogging.logError(e);}
|
||||
}
|
||||
}, "Run switch monitor.");
|
||||
runSwitchThread.start();
|
||||
|
@ -255,33 +264,41 @@ public class MovementFacade
|
|||
if(upperLimit.isHigh())
|
||||
{
|
||||
ErrorLogging.logError("DEBUG: Motor at highest point! Lowering to reset.");
|
||||
motorDirectionDown();
|
||||
motorOn();
|
||||
try{ Thread.sleep(500); }
|
||||
catch (Exception e){ ErrorLogging.logError(e); }
|
||||
motorOff();
|
||||
synchronized(lockObject)
|
||||
{
|
||||
motorDirectionDown();
|
||||
motorOn();
|
||||
try{ Thread.sleep(500); }
|
||||
catch (Exception e){ ErrorLogging.logError(e); }
|
||||
motorOff();
|
||||
}
|
||||
}
|
||||
ErrorLogging.logError("DEBUG: Moving motor to highest point.");
|
||||
motorDirectionUp();
|
||||
|
||||
motorOn();
|
||||
synchronized(lockObject)
|
||||
{
|
||||
motorOn();
|
||||
|
||||
for(counter = 0; counter < TIMEOUT; counter++)
|
||||
{
|
||||
try{ Thread.sleep(POLL_WAIT); } catch(Exception e){ ErrorLogging.logError(e); }
|
||||
if(upperLimit.isOn())
|
||||
{
|
||||
try{ Thread.sleep(1); } catch(Exception e){ErrorLogging.logError(e); }
|
||||
if(upperLimit.isOn()) break;
|
||||
for(counter = 0; counter < TIMEOUT; counter++)
|
||||
{
|
||||
try{ Thread.sleep(POLL_WAIT); } catch(Exception e){ ErrorLogging.logError(e); }
|
||||
if(upperLimit.isOn())
|
||||
{
|
||||
try{ Thread.sleep(1); } catch(Exception e){ErrorLogging.logError(e); }
|
||||
if(upperLimit.isOn()) break;
|
||||
}
|
||||
}
|
||||
motorOff();
|
||||
}
|
||||
motorOff();
|
||||
|
||||
if(counter < TIMEOUT)
|
||||
{
|
||||
ErrorLogging.logError("DEBUG: Motor returned after " + counter + " polls.");
|
||||
ErrorLogging.logError("DEBUG: --------------------------------------");
|
||||
return counter;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
ErrorLogging.logError("DEBUG: No motor return after 3 seconds.");
|
||||
|
@ -372,21 +389,25 @@ public class MovementFacade
|
|||
ErrorLogging.logError("DEBUG: Travel time: " + totalPollCount);
|
||||
ErrorLogging.logError("DEBUG: High speed poll count: " + highSpeedPolls);
|
||||
ErrorLogging.logError("DEBUG: =============================");
|
||||
motorOn();
|
||||
for(int i = 0; i < highSpeedPolls; i++)
|
||||
|
||||
synchronized(lockObject)
|
||||
{
|
||||
try{ Thread.sleep(POLL_WAIT); } catch(Exception e){ ErrorLogging.logError(e); }
|
||||
if(limitSense.isOn())
|
||||
motorOn();
|
||||
for(int i = 0; i < highSpeedPolls; i++)
|
||||
{
|
||||
try{ Thread.sleep(5); } catch(Exception e){ErrorLogging.logError(e); }
|
||||
if(limitSense.isOn())
|
||||
try{ Thread.sleep(POLL_WAIT); } catch(Exception e){ ErrorLogging.logError(e); }
|
||||
if(limitSense.isOn())
|
||||
{
|
||||
motorOff();
|
||||
break;
|
||||
try{ Thread.sleep(5); } catch(Exception e){ErrorLogging.logError(e); }
|
||||
if(limitSense.isOn())
|
||||
{
|
||||
motorOff();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
motorOff();
|
||||
}
|
||||
motorOff();
|
||||
|
||||
output = (limitSense.isOn() ? FinalState.UNSAFE : FinalState.SAFE);
|
||||
|
||||
|
@ -412,11 +433,14 @@ public class MovementFacade
|
|||
*/
|
||||
public static void pressButton()
|
||||
{
|
||||
ErrorLogging.logError("DEBUG: Pressing button...");
|
||||
pistonActivate.on();
|
||||
try{ Thread.sleep(1000); } catch(Exception e) {ErrorLogging.logError(e);};
|
||||
ErrorLogging.logError("DEBUG: Releasing button...");
|
||||
pistonActivate.off();
|
||||
synchronized(lockObject)
|
||||
{
|
||||
ErrorLogging.logError("DEBUG: Pressing button...");
|
||||
pistonActivate.on();
|
||||
try{ Thread.sleep(1000); } catch(Exception e) {ErrorLogging.logError(e);};
|
||||
ErrorLogging.logError("DEBUG: Releasing button...");
|
||||
pistonActivate.off();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
39
src/main/java/org/baxter/disco/ocr/Pair.java
Normal file
39
src/main/java/org/baxter/disco/ocr/Pair.java
Normal file
|
@ -0,0 +1,39 @@
|
|||
package org.baxter.disco.ocr;
|
||||
|
||||
/**
|
||||
* Standard read-only pair object.
|
||||
*
|
||||
* @author Blizzard Finnegan
|
||||
* @version 1.0.0, 20 Mar. 2023
|
||||
*/
|
||||
public class Pair<E,F>
|
||||
{
|
||||
/**
|
||||
* The first object in the pair.
|
||||
*/
|
||||
private final E first;
|
||||
|
||||
/**
|
||||
* The second object in the pair.
|
||||
*/
|
||||
private final F second;
|
||||
|
||||
/**
|
||||
* Default constructor.
|
||||
*/
|
||||
public Pair(E first, F second)
|
||||
{
|
||||
this.first = first;
|
||||
this.second = second;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the first value.
|
||||
*/
|
||||
public E first() { return first; }
|
||||
|
||||
/**
|
||||
* Getter for the second value.
|
||||
*/
|
||||
public F second() { return second; }
|
||||
}
|
Loading…
Add table
Reference in a new issue