Let's say I have a Student
class like so:
public class Student {
private String name;
private double height;
private double weight;
}
And I have a list of such students:
private List<Student> studentList;
Requirement:- I want to calculate Average Weight and Height of all the students.
Therefore, I want to write a method named findAverage
, where I can somehow send the fieldName
as argument and the method will fetch the data accordingly as follows:
public double findAverage(List<Student> studentList, String fieldName){
List<Double> numList = new Arraylist<>();
for(Student student: studentList){
numList.add(studentList.getFieldName()); // How do I map the correct field name here?
}
double total = 0.0;
for(Double item: numList){
total = item;
}
return total/(double)numList.size();
}
System.out.println("Average Height :- " findAverage(studentList, height));
System.out.println("Average Weight :- " findAverage(studentList, weight));
CodePudding user response:
Using java stream API you could use a function to extract the field and calculate the averages like so:
public static void main(String[] args) {
List<Student> students = ...;
System.out.println(getAverage(students, Student::getHeight));
System.out.println(getAverage(students, Student::getWeight));
}
private static double getAverage(Collection<Student> students, ToDoubleFunction<Student> toDoubleFunction) {
return students
.stream()
.mapToDouble(toDoubleFunction)
.average()
.orElse(Double.NaN);
}
CodePudding user response:
In the (common) case of writing where the property to access is not only determined at runtime but already at compile time, I would suggest to avoid reflection.
This means, instead of trying to directly extract values of fields by providing their name (fieldName
), you should provide a function like a ToDoubleFunction<Student>
or Function<Student, Double>
which extracts the double
value from a student.
Doing so will make your code more refactoring-friendly and errors will be detected at compile-time instead of runtime.
Your function would then look like this
public double findAverage(List<Student> studentList, ToDoubleFunction<Student> getValue) {
List<Double> numList = new Arraylist<>();
for(Student student: studentList){
numList.add(getValue.applyAsDouble(student));
}
return ...;
}
and you would apply it like this:
double avgHeight = findAverage(studentList, Student::getHeight);
double avgWeight = findAverage(studentList, Student::getWeight);
or (slightly longer):
double avgHeight = findAverage(studentList, student -> student.getHeight());
double avgWeight = findAverage(studentList, student -> student.getWeight());
CodePudding user response:
You can do this with reflection, but it's fragile. You could collapse the number of possible Exception
(s) in the throws
but I wanted to give fragile extra emphasis. You are assuming that private
fields are accessible from the caller; sometimes they are. But sometimes they are not. And it depends on packaging and Java version. Regardless,
public double findAverage(List<Student> studentList, String fieldName)
throws NoSuchFieldException, SecurityException,
IllegalArgumentException, IllegalAccessException
{
Field f = Student.class.getField(fieldName);
int count = 0;
double acc = 0;
for (Student student : studentList) {
acc = f.getDouble(student);
count ;
}
if (count > 0) {
return acc / count;
}
return 0;
}
CodePudding user response:
How about using Method reference to dynamically define the way to get the value of the field:
public class Student {
private String name;
private double height;
private double weight;
// note I define getters here, probably you'll have those anyway
public double getHeight() {return height;}
public double getWeight() {return weight;}
}
Now define the method of average calculation:
import java.util.function.ToDoubleFunction;
...
public double findAverage(List<Student> studentList, ToDoubleFunction<Student> func){
List<Double> numList = new ArrayList<>();
for(Student student: studentList){
// note this call!
numList.add(func.applyAsDouble(student));
}
// whatever you need for calculating the average, omitted
}
Now if you want to calculate the avg. of height you should:
List<Student> students = ...
findAverage(students, Student::getHeight);
for weight average you make the following call:
List<Student> students = ...
findAverage(students, Student::getWeight);
Now, although you haven't asked, but you can use streams to calculate the average as well. I've posted the original version because its easier to understand the usage of ToDoubleFunction
that way, but probably you would like to end up with the (arguably cleaner) implementation:
public double findAverage(List<Student> studentList, ToDoubleFunction<Student> func){
return studentList.stream()
.mapToDouble(func)
.average()
.orElse(0);
}
CodePudding user response:
You can add a condition inside for loop to determine which property to use, then remove the second for loop because it is unnecessary.
double total = 0;
for(Student student: studentList){
if(fieldName.equals("height")) {
numList.add(studentList.getHeight());
total = studentList.getHeight();
} else {
numList.add(studentList.getWeight());
total = studentList.getWeight();
}
}